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  }