github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/memmodel/prog.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 ( 8 "fmt" 9 "strings" 10 ) 11 12 const MaxThreads = 3 13 const MaxOps = 3 // Max ops per thread 14 const MaxTotalOps = MaxThreads * MaxOps 15 const MaxVar = MaxTotalOps / 2 16 17 type Prog struct { 18 Threads [MaxThreads]Thread 19 NumLoads int 20 } 21 22 type Thread struct { 23 Ops [MaxOps + 1]Op // Last Op is always OpExit. 24 } 25 26 type Op struct { 27 Type OpType 28 Var byte 29 ID byte // For OpLoad, the unique ID of the load. 30 } 31 32 type OpType byte 33 34 const ( 35 OpExit OpType = iota 36 OpStore 37 OpLoad 38 ) 39 40 type PC struct { 41 // TID is the thread ID. I is the instruction index. 42 TID, I int 43 } 44 45 func (p *Prog) OpAt(pc PC) Op { 46 return p.Threads[pc.TID].Ops[pc.I] 47 } 48 49 func (p *Prog) String() string { 50 out := []string{} 51 52 line := []string{} 53 nthr := len(p.Threads) 54 for tid := range p.Threads { 55 if p.Threads[tid].Ops[0].Type == OpExit { 56 nthr = tid 57 break 58 } 59 line = append(line, fmt.Sprintf("T%d", tid)) 60 } 61 out = append(out, strings.Join(line, "\t")) 62 63 for inst := 0; inst < MaxOps; inst++ { 64 line = nil 65 any := false 66 for tid := range p.Threads[:nthr] { 67 if p.Threads[tid].Ops[inst].Type == OpExit { 68 line = append(line, "") 69 continue 70 } 71 line = append(line, p.Threads[tid].Ops[inst].String()) 72 any = true 73 } 74 if !any { 75 break 76 } 77 out = append(out, strings.Join(line, "\t")) 78 } 79 80 return strings.Join(out, "\n") 81 } 82 83 func (o Op) String() string { 84 switch o.Type { 85 case OpExit: 86 return "noop" 87 88 case OpStore: 89 return fmt.Sprintf("st %d", o.Var) 90 91 case OpLoad: 92 return fmt.Sprintf("%c=ld %d", o.ID+'a', o.Var) 93 } 94 return "???" 95 } 96 97 // MemState stores the memory state of an executing program. It is a 98 // bit vector indexed by variable number. 99 type MemState uint64 100 101 func init() { 102 if MemState(1<<MaxVar) == 0 { 103 panic("MaxVar is too large to fit in MemState") 104 } 105 } 106 107 // Exec executes o in state s and returns the new memory state and, if 108 // the operation is a load, the result of the operation (either 0 or 109 // 1). 110 func (o Op) Exec(s MemState) (MemState, int) { 111 switch o.Type { 112 case OpExit: 113 return s, 0 114 case OpStore: 115 return s | (1 << o.Var), 0 116 case OpLoad: 117 return s, int((s >> o.Var) & 1) 118 } 119 panic("bad op") 120 } 121 122 func GenerateProgs() <-chan Prog { 123 // TODO: Making this perform well doesn't matter all that much 124 // since Prog execution is much more costly, but it would be 125 // nice if this generated programs in an order that preferred 126 // smaller programs with fewer variables. 127 ch := make(chan Prog) 128 go func() { 129 // TODO: We could take much more advantage of the 130 // symmetry between threads and between variables and 131 // the fact that each variable only needs to be stored 132 // once and that each variable needs at least one 133 // store and at least one load. 134 // 135 // 1. Choose the number of threads >= 2. 136 // 2. Choose the number of variables >= 1. 137 // 3. Choose the length of the first thread such that 138 // threadlen * nthreads >= nvars*2. 139 // 4. Generate thread programs as tuples in weakly 140 // decreasing order, which each tuple entry is a store 141 // or a load of a particular variable. A store always 142 // stores the next available variable. This is 143 // constrained by step 2 so that if there are no more 144 // variables all remaining operations are loads and if 145 // the number of remaining variables equals the number 146 // of remaining operations, all operations are stores. 147 // 148 // Other possible constraints: a thread that does just 149 // a load is not interesting; each variable needs at 150 // least one load on a different thread from the 151 // store. 152 // 153 // Or: 154 // 155 // 1. Choose the total number of operations >= 2. 156 // 157 // 2. Choose a "shape" of the program with weakly 158 // decreasing number of operations and total area 159 // #ops. 160 // 161 // 3. Choose the number of variables <= floor(#ops/2). 162 // 163 // 4. Place #variables store operations with 164 // increasing positions. 165 // 166 // 5. For each variable, place a load in a thread that 167 // does not store to that variable. (For later 168 // variables this may run out of possibilities.) 169 // 170 // 6. For each remaining instruction slots, place a 171 // load of a variable stored on a different thread. 172 173 var p Prog 174 genrec(&p, 0, 0, MaxOps, 0, 0, ch) 175 close(ch) 176 }() 177 return ch 178 } 179 180 func genrec(p *Prog, thr, inst, maxops int, nextstore, nextload byte, out chan<- Prog) { 181 if thr == MaxThreads { 182 // TODO: This check cuts 3x3 down from 2.5M to 15K and 183 // 4x3 down from 16.5B to 7.8M, so it really would be 184 // worth generating these so as to avoid the silly 185 // ones. OTOH, 4x3 only takes 9 minutes to generate, 186 // so this might not be the slow part in the end! 187 if !silly(p, int(nextstore)) { 188 p.NumLoads = int(nextload) 189 out <- *p 190 } 191 return 192 } 193 194 nthr, ninst := thr, inst+1 195 if ninst == maxops { 196 nthr, ninst = nthr+1, 0 197 } 198 199 op := &p.Threads[thr].Ops[inst] 200 for opt := OpExit; opt <= OpLoad; opt++ { 201 op.Type = opt 202 203 switch opt { 204 case OpExit: 205 op.Var = 0 206 if inst == 0 { 207 // No more threads. 208 if thr < 2 { 209 // There need to be at least 210 // two threads for this to be 211 // interesting. 212 continue 213 } 214 genrec(p, MaxThreads, 0, 0, nextstore, nextload, out) 215 } else { 216 // Move on to the next thread. Limit 217 // it to at most the number of 218 // operations in this thread so thread 219 // lengths are weakly decreasing. 220 genrec(p, thr+1, 0, inst, nextstore, nextload, out) 221 } 222 223 case OpLoad: 224 op.ID = nextload 225 for op.Var = 0; op.Var < MaxVar; op.Var++ { 226 // Move on to the next instruction. 227 genrec(p, nthr, ninst, maxops, nextstore, nextload+1, out) 228 } 229 op.ID = 0 230 231 case OpStore: 232 // There's one store per variable and since 233 // the variable naming doesn't matter, we 234 // assign store variables in increasing order. 235 if nextstore == MaxVar { 236 // Out of variables to store. If we 237 // keep going there won't be room for 238 // loads of all of the variables. 239 continue 240 } 241 op.Var = nextstore 242 // Move on to the next instructions. 243 genrec(p, nthr, ninst, maxops, nextstore+1, nextload, out) 244 } 245 } 246 op.Type = OpExit 247 op.Var = 0 248 } 249 250 func silly(p *Prog, nvars int) bool { 251 var storeThread [MaxVar]int 252 for tid := range p.Threads { 253 for _, op := range p.Threads[tid].Ops { 254 if op.Type == OpStore { 255 storeThread[op.Var] = tid 256 } 257 } 258 } 259 for tid := range p.Threads { 260 for _, op := range p.Threads[tid].Ops { 261 if op.Type == OpLoad { 262 if storeThread[op.Var] == 0 { 263 // Load of a variable never 264 // stored. Silly. 265 return true 266 } else if storeThread[op.Var] != tid { 267 storeThread[op.Var] = -1 268 } 269 } 270 } 271 } 272 for v := 0; v < nvars; v++ { 273 if storeThread[v] != -1 { 274 // Store without load on another thread. Silly. 275 return true 276 } 277 } 278 return false 279 }