github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/go-weave/models/yuasa.go (about)

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build ignore
     6  
     7  // yuasa is a model of several variants of Yuasa-style deletion
     8  // barriers intended to eliminate stack re-scanning.
     9  package main
    10  
    11  import (
    12  	"bytes"
    13  	"fmt"
    14  
    15  	"github.com/aclements/go-misc/go-weave/amb"
    16  	"github.com/aclements/go-misc/go-weave/weave"
    17  )
    18  
    19  type barrierType int
    20  
    21  const (
    22  	// yuasaBarrier is a Yuasa-style deletion barrier. It requires
    23  	// stackBeforeHeap, but does not require rescanStacks.
    24  	yuasaBarrier barrierType = iota
    25  
    26  	// dijkstraYuasaBarrier is a combined Dijkstra-style insertion
    27  	// barrier and Yuasa-style deletion barrier. It does not
    28  	// require stackBeforeHeap or rescanStacks.
    29  	dijkstraYuasaBarrier
    30  
    31  	// conditionalDijkstraYuasaBarrier is like
    32  	// dijkstraYuasaBarrier before all stacks are blacked, and
    33  	// like yuasaBarrier after stacks have been blacked. It does
    34  	// not require stackBeforeHeap or rescanStacks.
    35  	conditionalDijkstraYuasaBarrier
    36  
    37  	// dijkstraBarrier is a Dijkstra-style insertion barrier. It
    38  	// does not require stackBeforeHeap, but it does require
    39  	// rescanStacks.
    40  	dijkstraBarrier
    41  )
    42  
    43  // barrier indicates the type of write barrier to use.
    44  const barrier = conditionalDijkstraYuasaBarrier
    45  
    46  // stackBeforeHeap indicates that all stacks must be blackened before
    47  // any heap objects are blackened.
    48  const stackBeforeHeap = false
    49  
    50  // rescanStacks indicates that stacks must be re-scanned during STW
    51  // mark termination.
    52  const rescanStacks = false
    53  
    54  // ptr is a memory pointer, as an index into mem. 0 is the nil
    55  // pointer.
    56  type ptr int
    57  
    58  // obj is an object in memory. An object in the "global" or "heap"
    59  // region of memory must not point to an object in the "stack" region
    60  // of memory.
    61  type obj [2]ptr
    62  
    63  // mem is the memory, including both the heap and stacks. mem[0] is
    64  // unused (it's the nil slot)
    65  //
    66  // mem[stackBase+i] for i < numThreads is the stack for thread i.
    67  //
    68  // mem[globalRoot] is the global root.
    69  //
    70  // mem[heapBase:] is the heap.
    71  var mem []obj
    72  
    73  // marked is the set of mark bits. marked[i] corresponds to mem[i].
    74  var marked []bool
    75  
    76  // work is the work list. This is the set of grey objects.
    77  var work []ptr
    78  
    79  const numThreads = 2
    80  
    81  const stackBase ptr = 1
    82  const globalRoot ptr = stackBase + numThreads
    83  const heapBase ptr = globalRoot + 1
    84  const heapCount = 3
    85  
    86  var world weave.RWMutex
    87  var stackLocks [numThreads]weave.Mutex
    88  
    89  // rootCount is the number of unscanned roots.
    90  var rootCount int
    91  
    92  const verbose = false
    93  
    94  var sched = weave.Scheduler{Strategy: &amb.StrategyRandom{}}
    95  
    96  func main() {
    97  	sched.Run(func() {
    98  		if verbose {
    99  			print("start:")
   100  		}
   101  		// Create an ambiguous memory.
   102  		//
   103  		// TODO: Tons of these are isomorphic.
   104  		mem = make([]obj, heapBase+heapCount)
   105  		for i := 1; i < len(mem); i++ {
   106  			mem[i] = obj{ambHeapPointer(), ambHeapPointer()}
   107  		}
   108  		marked = make([]bool, len(mem))
   109  		if verbose {
   110  			println(stringMem(mem, marked))
   111  		}
   112  		sched.Tracef("memory: %s", stringMem(mem, marked))
   113  		world = weave.RWMutex{} // Belt and suspenders.
   114  		for i := range stackLocks {
   115  			stackLocks[i] = weave.Mutex{}
   116  		}
   117  		rootCount = numThreads + 1
   118  
   119  		// Start mutators.
   120  		for i := 0; i < numThreads; i++ {
   121  			i := i
   122  			sched.Go(func() { mutator(i) })
   123  		}
   124  
   125  		if stackBeforeHeap {
   126  			sched.Trace("scanning stacks")
   127  			// Scan stacks and global roots. Complete this
   128  			// before allowing any blackening of the heap.
   129  			for i := stackBase; i < stackBase+numThreads; i++ {
   130  				scan(i)
   131  				marked[i] = true
   132  			}
   133  			scan(globalRoot)
   134  			marked[globalRoot] = true
   135  			sched.Trace("done scanning stacks")
   136  		} else {
   137  			// Grey stacks and global roots. Drain will
   138  			// scan them.
   139  			for i := stackBase; i < stackBase+numThreads; i++ {
   140  				shade(i)
   141  			}
   142  			shade(globalRoot)
   143  		}
   144  
   145  		// Blacken heap.
   146  		drain()
   147  
   148  		// Wait for write barriers to complete.
   149  		world.Lock()
   150  		defer world.Unlock()
   151  
   152  		if rescanStacks {
   153  			sched.Trace("rescanning stacks")
   154  			// Rescan stacks. (The write barrier applies
   155  			// to globals, so we don't need to rescan
   156  			// globalRoot.)
   157  			for i := stackBase; i < stackBase+numThreads; i++ {
   158  				marked[i] = false
   159  				shade(i)
   160  			}
   161  			drain()
   162  			sched.Trace("done rescanning stacks")
   163  		}
   164  
   165  		// Check that everything is marked.
   166  		if verbose {
   167  			println(stringMem(mem, marked))
   168  		}
   169  		sched.Tracef("memory: %s", stringMem(mem, marked))
   170  		checkmark()
   171  	})
   172  }
   173  
   174  type pointerSet int
   175  
   176  const (
   177  	// pointerNil indicates that ambPointer can return a nil
   178  	// pointer.
   179  	pointerNil pointerSet = 1 << iota
   180  
   181  	// pointerStack indicates that ambPointer can return a pointer
   182  	// to the stack.
   183  	pointerStack
   184  
   185  	// pointerReachable indicates that ambPointer can return a
   186  	// pointer to a reachable heap or global object.
   187  	pointerReachable
   188  
   189  	// pointerHeap indicates that ambPointer can return a pointer
   190  	// to any global or heap object.
   191  	pointerHeap
   192  )
   193  
   194  // ambPointer returns an ambiguous pointer from the union of the
   195  // specified sets. If ps&(pointerStack|pointerReachable) != 0, tid
   196  // must specify the thread ID of the stack.
   197  func ambPointer(ps pointerSet, tid int) ptr {
   198  	if ps&pointerReachable == 0 {
   199  		// Easy/fast case.
   200  		count := 0
   201  		if ps&pointerNil != 0 {
   202  			count++
   203  		}
   204  		if ps&pointerStack != 0 {
   205  			count++
   206  		}
   207  		if ps&pointerHeap != 0 {
   208  			count += 1 + heapCount
   209  		}
   210  		x := sched.Amb(count)
   211  		if ps&pointerNil != 0 {
   212  			if x == 0 {
   213  				return 0
   214  			}
   215  			x--
   216  		}
   217  		if ps&pointerStack != 0 {
   218  			if x == 0 {
   219  				return stackBase + ptr(tid)
   220  			}
   221  			x--
   222  		}
   223  		if x == 0 {
   224  			return globalRoot
   225  		}
   226  		return heapBase + ptr(x-1)
   227  	}
   228  
   229  	// Tricky case. Create a mask of the pointers we're interested in.
   230  	marked := make([]bool, len(mem))
   231  	mark(globalRoot, marked)
   232  	mark(stackBase+ptr(tid), marked)
   233  	if ps&pointerNil != 0 {
   234  		marked[0] = true
   235  	}
   236  	if ps&pointerStack == 0 {
   237  		marked[stackBase+ptr(tid)] = false
   238  	}
   239  
   240  	// Select a marked pointer.
   241  	nmarked := 0
   242  	for _, m := range marked {
   243  		if m {
   244  			nmarked++
   245  		}
   246  	}
   247  	x := sched.Amb(nmarked)
   248  	for i, m := range marked {
   249  		if m {
   250  			if x == 0 {
   251  				return ptr(i)
   252  			}
   253  			x--
   254  		}
   255  	}
   256  	panic("not reachable")
   257  }
   258  
   259  // ambHeapPointer returns nil or an ambiguous heap or global pointer.
   260  func ambHeapPointer() ptr {
   261  	return ambPointer(pointerNil|pointerHeap, -1)
   262  }
   263  
   264  // scan scans obj, shading objects that obj re
   265  func scan(obj ptr) {
   266  	sched.Tracef("scan(%v)", obj)
   267  	if stackBase <= obj && obj < stackBase+numThreads {
   268  		stackLocks[obj-stackBase].Lock()
   269  		defer stackLocks[obj-stackBase].Unlock()
   270  	}
   271  	for i := range mem[obj] {
   272  		p := mem[obj][i]
   273  		sched.Sched()
   274  		shade(p)
   275  	}
   276  	if stackBase <= obj && obj < stackBase+numThreads || obj == globalRoot {
   277  		rootCount--
   278  		sched.Tracef("roots remaining = %d", rootCount)
   279  	}
   280  }
   281  
   282  // shade makes obj grey if it is white.
   283  func shade(obj ptr) {
   284  	if obj != 0 && !marked[obj] {
   285  		sched.Tracef("shade(%v)", obj)
   286  		marked[obj] = true
   287  		work = append(work, obj)
   288  	}
   289  }
   290  
   291  // drain scans objects in the work queue until the queue is empty.
   292  func drain() {
   293  	for len(work) > 0 {
   294  		// Pick an arbitrary object to scan.
   295  		which := sched.Amb(len(work))
   296  		p := work[which]
   297  		copy(work[which:], work[which+1:])
   298  		work = work[:len(work)-1]
   299  
   300  		scan(p)
   301  	}
   302  }
   303  
   304  // writePointer implements obj[slot] = val.
   305  func writePointer(obj ptr, slot int, val ptr) {
   306  	// TODO: Check that GC is still running?
   307  
   308  	// Synchronize with STW. This blocks STW from happening while
   309  	// we're in the barrier and blocks this goroutine if we're
   310  	// already in STW.
   311  	world.RLock()
   312  	defer world.RUnlock()
   313  
   314  	if obj == 0 {
   315  		panic("nil pointer write")
   316  	}
   317  
   318  	if stackBase <= obj && obj < stackBase+numThreads {
   319  		mem[obj][slot] = val
   320  		sched.Tracef("stack write %v[%d] = %v", obj, slot, val)
   321  		sched.Sched()
   322  		return
   323  	}
   324  
   325  	sched.Tracef("start %v[%d] = %v", obj, slot, val)
   326  
   327  	switch barrier {
   328  	case yuasaBarrier:
   329  		old := mem[obj][slot]
   330  		sched.Sched()
   331  		shade(old)
   332  
   333  	case dijkstraYuasaBarrier:
   334  		old := mem[obj][slot]
   335  		sched.Sched()
   336  		shade(old)
   337  		shade(val)
   338  
   339  	case conditionalDijkstraYuasaBarrier:
   340  		old := mem[obj][slot]
   341  		sched.Sched()
   342  		shade(old)
   343  		if rootCount > 0 {
   344  			shade(val)
   345  		}
   346  
   347  	case dijkstraBarrier:
   348  		shade(val)
   349  	}
   350  
   351  	mem[obj][slot] = val
   352  	sched.Tracef("done %v[%d] = %v", obj, slot, val)
   353  	sched.Sched()
   354  }
   355  
   356  // mutator is a single mutator goroutine running on stack stackBase+tid.
   357  // It shuffles pointers between the heap and stack.
   358  func mutator(tid int) {
   359  	stackptr := stackBase + ptr(tid)
   360  
   361  	for i := 0; i < 2; i++ {
   362  		// Take the stack lock to indicate that we're not at a
   363  		// safe point. There's no safe point between reading
   364  		// src and writing pointer since in the model we can't
   365  		// communicate the pointer we're looking at to the GC.
   366  		//
   367  		// Somewhat surprisingly, it's actually necessary to
   368  		// model this. Otherwise stack writes that race with
   369  		// the stack scan can hide pointers.
   370  		stackLocks[tid].Lock()
   371  
   372  		// Write a nil, global, or heap pointer to the stack, global,
   373  		// or heap, or a stack pointer to the stack.
   374  		src := ambPointer(pointerNil|pointerStack|pointerReachable, tid)
   375  		sched.Sched()
   376  		var dst ptr
   377  		if src == stackptr {
   378  			// Stack pointers can only be written to the stack.
   379  			dst = stackptr
   380  		} else {
   381  			// Non-stack pointers can be written to stack, global,
   382  			// or heap.
   383  			dst = ambPointer(pointerStack|pointerReachable, tid)
   384  		}
   385  		writePointer(dst, sched.Amb(2), src)
   386  
   387  		// We're at a safe point again.
   388  		stackLocks[tid].Unlock()
   389  	}
   390  }
   391  
   392  // mark sets marked[i] for every object i reachable from p (including
   393  // p itself). This is NOT preemptible.
   394  func mark(p ptr, marked []bool) {
   395  	if p == 0 || marked[p] {
   396  		return
   397  	}
   398  	marked[p] = true
   399  	for i := range mem[p] {
   400  		mark(mem[p][i], marked)
   401  	}
   402  }
   403  
   404  // checkmark checks that all objects reachable from the roots are
   405  // marked.
   406  func checkmark() {
   407  	checkmarked := make([]bool, len(mem))
   408  	for i := stackBase; i < stackBase+numThreads; i++ {
   409  		mark(i, checkmarked)
   410  	}
   411  	mark(globalRoot, checkmarked)
   412  
   413  	for i := range marked {
   414  		if checkmarked[i] && !marked[i] {
   415  			panic(fmt.Sprintf("object not marked: %v", i))
   416  		}
   417  	}
   418  }
   419  
   420  // stringMem stringifies a memory with marks.
   421  func stringMem(mem []obj, marked []bool) string {
   422  	var buf bytes.Buffer
   423  	for i := 1; i < len(mem); i++ {
   424  		if marked[i] {
   425  			buf.WriteString("*")
   426  		} else {
   427  			buf.WriteString(" ")
   428  		}
   429  		fmt.Fprint(&buf, i, "->", mem[i][0], ",", mem[i][1], " ")
   430  	}
   431  	return buf.String()
   432  }