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.