github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/memmodel/hb.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  package main
     6  
     7  import "fmt"
     8  
     9  // HBModel is a generalized memory model with a plug-in happens-before
    10  // graph generator.
    11  //
    12  // This model works by generating all sequential interleavings of the
    13  // program, constructing the happens-before graph for each
    14  // interleaving, and computing the possible outcomes for each load
    15  // operation based on the happens-before graph (notably, not based on
    16  // the sequential order). If a load happens concurrently with a store
    17  // (that is, it neither happens before nor happens after), it can read
    18  // either 0 or 1, so each sequential interleaving can produce one or
    19  // more outcomes.
    20  type HBModel struct {
    21  	Gen HBGenerator
    22  }
    23  
    24  func (m HBModel) String() string {
    25  	return m.Gen.String() + " (HB)"
    26  }
    27  
    28  // HBGenerator generates a happens-before graph.
    29  //
    30  // TODO: Given how stateless this is, we could precompute all n^2
    31  // results.
    32  type HBGenerator interface {
    33  	// HappensBefore returns whether instruction i globally
    34  	// happens before instruction j, assuming that i was executed
    35  	// before j.
    36  	//
    37  	// It returns HBHappensBefore if i globally happens before j.
    38  	// It does *not* return program order happens-before relations
    39  	// unless they are part of the global happens-before graph.
    40  	// Each thread implicitly has a local happens-before graph
    41  	// that combines the global happens-before graph with the
    42  	// local program order.
    43  	//
    44  	// It returns HBConcurrent if i happens concurrently with j (i
    45  	// neither happens before nor after j). However, the
    46  	// transitive closure of other happens-before relations may
    47  	// still imply that i happens before j.
    48  	//
    49  	// It returns HBConditional if i happens before j only if j
    50  	// observes i.
    51  	HappensBefore(p *Prog, i, j PC) HBType
    52  
    53  	// String returns a string representation of this HBGenerator.
    54  	String() string
    55  }
    56  
    57  type HBType int
    58  
    59  const (
    60  	HBConcurrent HBType = iota
    61  	HBHappensBefore
    62  	HBConditional
    63  )
    64  
    65  func (m HBModel) Eval(p *Prog, outcomes *OutcomeSet) {
    66  	var seqBuf [MaxTotalOps]PC
    67  	var nodeBuf [MaxTotalOps]hbGraphNode
    68  	g := hbGlobal{
    69  		p:        p,
    70  		outcomes: outcomes,
    71  		model:    m,
    72  		sequence: seqBuf[:0],
    73  		graph:    hbGraph{nodeBuf[:0]},
    74  	}
    75  	outcomes.Reset(p)
    76  	g.rec(hbState{})
    77  }
    78  
    79  // hbGlobal stores state that is global to an evaluation.
    80  type hbGlobal struct {
    81  	p        *Prog
    82  	outcomes *OutcomeSet
    83  	model    HBModel
    84  
    85  	// sequence is the current program counter sequence. PCs are
    86  	// pushed and popped as rec explores interleavings.
    87  	sequence []PC
    88  
    89  	// graph is the current happens-before graph. This is built up
    90  	// together with sequence.
    91  	graph hbGraph
    92  
    93  	// vars records the indexes in sequence of loads and stores
    94  	// that have executed on each variable.
    95  	vars [MaxVar]varInfo
    96  }
    97  
    98  // An hbGraphNode represents the set of in-edges for a single node in
    99  // the global happens-before graph. If bit i is set, then node i
   100  // globally happens-before this node.
   101  type hbGraphNode uint16
   102  
   103  func init() {
   104  	if hbGraphNode(1<<MaxTotalOps) == 0 {
   105  		panic("MaxTotalOps is too large to fit in hbGraphNode")
   106  	}
   107  }
   108  
   109  // An hbGraph is a global happens-before graph represented in matrix
   110  // form. Nodes correspond to instructions and are indexed by their
   111  // position in the execution sequence. Each thread has a local
   112  // happens-before graph that is the union of the global happens-before
   113  // graph and the local program order.
   114  type hbGraph struct {
   115  	nodes []hbGraphNode
   116  }
   117  
   118  // happenedBefore returned true if i happened before j.
   119  func (g *hbGraph) happenedBefore(i, j int) bool {
   120  	return g.nodes[j]&(1<<uint(i)) != 0
   121  }
   122  
   123  type varInfo struct {
   124  	// stores are the indexes of store operations to this variable
   125  	// in sequence.
   126  	stores []int
   127  
   128  	// loads are the indexes of load operations from this variable
   129  	// in sequence.
   130  	loads []int
   131  }
   132  
   133  // hbState stores the state of a program at a single point during
   134  // execution.
   135  type hbState struct {
   136  	// Program state.
   137  	pcs [MaxThreads]int
   138  
   139  	// allow0 and allow1 indicate whether each load can return 0
   140  	// or 1, respectively. It's possible that a load can return
   141  	// either, in which case both will be set. By the end of an
   142  	// execution, at least of the two possibilities will be set
   143  	// for each load operation.
   144  	allow0, allow1 Outcome
   145  }
   146  
   147  func (g *hbGlobal) rec(s hbState) {
   148  	// To reduce repeated work, we construct the happens-before
   149  	// graph and outcome set incrementally and as early in the
   150  	// recursion as possible. The happens-before graph must be
   151  	// consistent with the sequential order, so every time we
   152  	// select the next instruction, we can immediately add it to
   153  	// the happens-before graph and compute its full set of
   154  	// in-edges (because these must all come from already executed
   155  	// instructions).
   156  	//
   157  	// Because we build the graph incrementally, we can also
   158  	// compute possible outcomes incrementally. For each load, we
   159  	// need to answer whether the store to that variable happened
   160  	// before, after, or concurrently with the load. When we
   161  	// execute the load, if the store has already been executed,
   162  	// we can answer whether it happened before or concurrently
   163  	// with the load. If the store hasn't been executed yet, it
   164  	// may happen after or concurrently with the load; once we
   165  	// execute the store, we go back and determine this for all of
   166  	// the previously executed loads of that variable.
   167  
   168  	// Pick an op to execute next.
   169  	any := false
   170  	for tid := range g.p.Threads {
   171  		op := g.p.Threads[tid].Ops[s.pcs[tid]]
   172  		if op.Type == OpExit {
   173  			continue
   174  		}
   175  
   176  		any = true
   177  		ns := s
   178  		thisPC := PC{tid, s.pcs[tid]}
   179  		g.sequence = append(g.sequence, thisPC)
   180  		ns.pcs[tid]++
   181  
   182  		// Add a node for this instruction to the global
   183  		// happens-before graph.
   184  		var node hbGraphNode
   185  		this := len(g.sequence) - 1
   186  		// Compute the in-edges and the transitive closure.
   187  		for prev := this - 1; prev >= 0; prev-- {
   188  			if node&(1<<uint(prev)) != 0 {
   189  				// By transitive closure, we already
   190  				// know prev happens-before this.
   191  				continue
   192  			}
   193  
   194  			if g.model.Gen.HappensBefore(g.p, g.sequence[prev], thisPC) == HBHappensBefore {
   195  				// Add global "prev -> this" edge and
   196  				// the transitive closure.
   197  				node |= (1 << uint(prev)) | g.graph.nodes[prev]
   198  			}
   199  		}
   200  		g.graph.nodes = append(g.graph.nodes, node)
   201  
   202  		var varOpArray *[]int
   203  		switch op.Type {
   204  		case OpStore:
   205  			// Find loads to this variable that have
   206  			// already executed.
   207  			for _, ld := range g.vars[op.Var].loads {
   208  				// If this load locally happened
   209  				// before the store, the load must be
   210  				// 0 (which we've already recorded).
   211  				if g.sequence[ld].TID == tid || g.graph.happenedBefore(ld, this) {
   212  					continue
   213  				}
   214  				// If the store conditionally happens
   215  				// before the load, then, again, the
   216  				// load must be 0 because making it 1
   217  				// would mean it observed the store,
   218  				// which would add a happens-before
   219  				// graph that goes against the
   220  				// sequential order.
   221  				if g.model.Gen.HappensBefore(g.p, thisPC, g.sequence[ld]) == HBConditional {
   222  					continue
   223  				}
   224  				// Otherwise, the load happened
   225  				// concurrently with the store, so it
   226  				// can be 0 or 1.
   227  				ldpc := g.sequence[ld]
   228  				ns.allow1.Set(g.p.OpAt(ldpc), 1)
   229  			}
   230  
   231  			varOpArray = &g.vars[op.Var].stores
   232  
   233  		case OpLoad:
   234  			// Find stores to this variable that have
   235  			// already executed.
   236  			var mustBe1, any bool
   237  			var lastStore int
   238  			for _, st := range g.vars[op.Var].stores {
   239  				any = true
   240  				lastStore = st
   241  				// If this store locally happened
   242  				// before the load, the load must be
   243  				// 1.
   244  				if g.sequence[st].TID == tid || g.graph.happenedBefore(st, this) {
   245  					mustBe1 = true
   246  					break
   247  				}
   248  			}
   249  
   250  			if mustBe1 {
   251  				ns.allow1.Set(op, 1)
   252  			} else if any {
   253  				// There were stores, but none
   254  				// happened before the load, then the
   255  				// load happened concurrently with the
   256  				// stores, so it may be 0 or 1.
   257  				if g.model.Gen.HappensBefore(g.p, g.sequence[lastStore], thisPC) == HBConditional {
   258  					// There are two
   259  					// possibilities: 1) the load
   260  					// doesn't observe the store,
   261  					// then it must read 0, but
   262  					// this doesn't introduce an
   263  					// edge.
   264  					ns0 := ns
   265  					ns0.allow0.Set(op, 1)
   266  					g.rec(ns0)
   267  
   268  					// Or 2) the load observes the
   269  					// store, so it must read 1
   270  					// and introduces an edge.
   271  					node |= (1 << uint(lastStore)) | g.graph.nodes[lastStore]
   272  					g.graph.nodes[len(g.graph.nodes)-1] = node
   273  					ns.allow1.Set(op, 1)
   274  				} else {
   275  					// The load and store really
   276  					// are concurrent, so anything
   277  					// can happen.
   278  					ns.allow0.Set(op, 1)
   279  					ns.allow1.Set(op, 1)
   280  				}
   281  			} else {
   282  				// Otherwise, it's definitely possible
   283  				// for it to be 0. There may also be a
   284  				// future store that happens
   285  				// concurrently; if we find one, we'll
   286  				// allow 1 at that point.
   287  				ns.allow0.Set(op, 1)
   288  				varOpArray = &g.vars[op.Var].loads
   289  			}
   290  
   291  		default:
   292  			panic("unknown op")
   293  		}
   294  
   295  		// Associate this operation with the variable.
   296  		if varOpArray != nil {
   297  			*varOpArray = append(*varOpArray, this)
   298  		}
   299  
   300  		g.rec(ns)
   301  
   302  		g.sequence = g.sequence[:len(g.sequence)-1]
   303  		g.graph.nodes = g.graph.nodes[:len(g.graph.nodes)-1]
   304  		if varOpArray != nil {
   305  			*varOpArray = (*varOpArray)[:len(*varOpArray)-1]
   306  		}
   307  	}
   308  	if !any {
   309  		// This execution is done. Expand out the full set of
   310  		// possible outcomes.
   311  		if (s.allow0|s.allow1)>>uint(g.p.NumLoads) != 0 {
   312  			panic("more outcome bits than loads")
   313  		}
   314  		if (s.allow0^(1<<uint(g.p.NumLoads)-1))&(s.allow1^(1<<uint(g.p.NumLoads)-1)) != 0 {
   315  			panic(fmt.Sprintf("no outcome for load: allow0=0x%x allow1=0x%x in program:\n%s", s.allow0, s.allow1, g.p))
   316  		}
   317  		g.addOutcomes(0, s.allow0, s.allow1, 0)
   318  	}
   319  }
   320  
   321  func (g *hbGlobal) addOutcomes(bit uint, allow0, allow1, out Outcome) {
   322  	if (allow0&allow1)>>bit == 0 {
   323  		// The rest are deterministic (or we're at the end).
   324  		out |= allow1 >> bit << bit
   325  		g.outcomes.Add(out)
   326  		return
   327  	}
   328  
   329  	// Copy deterministic bits.
   330  	for (allow0&allow1)&(1<<bit) == 0 {
   331  		if allow1&(1<<bit) != 0 {
   332  			out |= 1 << bit
   333  		}
   334  		bit++
   335  	}
   336  
   337  	// Bit is now non-deterministic. Go both ways.
   338  	g.addOutcomes(bit+1, allow0, allow1, out)
   339  	g.addOutcomes(bit+1, allow0, allow1, out|(1<<bit))
   340  }
   341  
   342  // SC: i happens before j if and only if i < j.
   343  
   344  // TSO: i happens before j if i < j and 1) both i and j are stores, or
   345  // 2) both i and j are loads, or 3) i is a load and j is a store, or
   346  // 4) i stores to X and j loads from X.
   347  
   348  // RMO: i happens before j if i < j and i stores to X and j loads from
   349  // X.