| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package ssacompile | |
| // This file contains the mem2reg pass. | |
| // mem2reg promotes memory operations to register operations. | |
| // This is a classic compiler optimization that can significantly | |
| // improve the performance of generated code by reducing the number | |
| // of memory accesses. | |
| // | |
| // The algorithm identifies memory stores (OpStore) to stack slots | |
| // that are followed by loads (OpLoad) from the same stack slot, | |
| // with no intervening stores to that slot. The loads are then | |
| // replaced with the value that was stored. | |
| import ( | |
| "cmd/compile/internal/base" | |
| "cmd/compile/internal/ir" | |
| "cmd/compile/internal/ssa" | |
| "cmd/compile/internal/ssa/ssaop" | |
| "cmd/compile/internal/types" | |
| "internal/buildcfg" | |
| "slices" | |
| ) | |
| type useType int | |
| const ( | |
| useLoad useType = 1 << iota // used as the address to load from | |
| useStore // used as the address to store to | |
| useOffset // pointer arithmetic (OpOffPtr) | |
| useCopy // uses propagated through a copy | |
| useZero // used in OpZero | |
| useMove // used in OpMove | |
| useOther // includes escaping uses and OpPtrIndex | |
| ) | |
| // useTable wraps the shared reverse-use table (see uses.go) so that | |
| // queries about values created after the table was built - their IDs | |
| // are out of range - report no uses instead of panicking. The mem2reg | |
| // and decomposeAddr passes ask about such values after their rewrites. | |
| type useTable struct { | |
| useInfo | |
| } | |
| // of returns the uses of v recorded in u, or nil if v postdates the table. | |
| func (u *useTable) of(v *ssa.Value) []*ssa.Value { | |
| if int(v.ID) >= len(u.starts) { | |
| return nil | |
| } | |
| return u.get(v) | |
| } | |
| // classifyUses analyzes how a pointer is used and returns a bitmask of use types. | |
| func classifyUses(v *ssa.Value, uses *useTable) useType { | |
| q := []*ssa.Value{v} | |
| var u useType | |
| for len(q) > 0 { | |
| curr := q[len(q)-1] | |
| q = q[:len(q)-1] | |
| for _, use := range uses.of(curr) { | |
| switch use.Op { | |
| case ssaop.OpCopy: | |
| u |= useCopy | |
| q = append(q, use) | |
| case ssaop.OpOffPtr: | |
| u |= useOffset | |
| q = append(q, use) | |
| case ssaop.OpLoad: | |
| u |= useLoad | |
| case ssaop.OpStore: | |
| if curr == use.Args[1] { | |
| // Storing the address as value. | |
| u |= useOther | |
| } else { | |
| u |= useStore | |
| } | |
| case ssaop.OpZero: | |
| u |= useZero | |
| case ssaop.OpMove: | |
| u |= useMove | |
| default: | |
| // TODO: handle more cases? | |
| // e.g. AddPtr, SubPtr, PtrIndex, NilCheck, EqPtr, NeqPtr | |
| u |= useOther | |
| } | |
| } | |
| } | |
| return u | |
| } | |
| // variableDemographic summarizes how the address of an auto variable is | |
| // used, over all LocalAddr uses of that variable. | |
| type variableDemographic struct { | |
| // ls is true when the address of this variable is only used by Load | |
| // and Store, and no VarLive is applied on the name. | |
| ls bool | |
| // lszmco is true when the address of this variable is only used by | |
| // Load, Store, Zero, Move, Copy and OffPtr, and no VarLive is | |
| // applied on the name. | |
| lszmco bool | |
| varDefs []*ssa.Value // All the VarDefs on this name. | |
| } | |
| // variableDemographics analyzes f and returns the variable demographics data of f. | |
| // | |
| // demographics is the map from names to their demographics. | |
| // localAddrs is the LocalAddr ssa values of the names in demographics. | |
| // u tracks the uses of the values in f (see uses.go); it is nil, and | |
| // nothing is allocated, when the function has no candidate variables. | |
| // | |
| // The caller must free u (u.free(f)) when non-nil. | |
| func variableDemographics(f *ssa.Func) ( | |
| demographics map[ssa.Aux]*variableDemographic, | |
| localAddrs []*ssa.Value, | |
| u *useTable) { | |
| // Fast path: most functions have no candidate LocalAddr at all, and | |
| // for them the pass chain has nothing to do. Detect that with a scan | |
| // that allocates nothing, so those functions pay (almost) nothing. | |
| found := false | |
| for _, b := range f.Blocks { | |
| for _, c := range b.ControlValues() { | |
| if c.Op == ssaop.OpOffPtr || c.Op == ssaop.OpLocalAddr { | |
| f.Fatalf("unexpected pointer value in block control") | |
| } | |
| } | |
| for _, v := range b.Values { | |
| if v.Op == ssaop.OpLocalAddr { | |
| if n := v.Aux.(*ir.Name); n.Class == ir.PAUTO || isABIInternalParam(f, n) { | |
| found = true | |
| break | |
| } | |
| } | |
| } | |
| if found { | |
| break | |
| } | |
| } | |
| if !found { | |
| return nil, nil, nil | |
| } | |
| // Build the reverse-use table using the shared cache-backed | |
| // infrastructure from uses.go. | |
| u = &useTable{uses(f)} | |
| varLives := map[ssa.Aux]bool{} | |
| demographics = make(map[ssa.Aux]*variableDemographic) | |
| for _, b := range f.Blocks { | |
| for _, v := range b.Values { | |
| switch v.Op { | |
| case ssaop.OpVarLive: | |
| varLives[v.Aux] = true | |
| case ssaop.OpVarDef: | |
| d := demographics[v.Aux] | |
| if d == nil { | |
| d = &variableDemographic{ls: true, lszmco: true} | |
| demographics[v.Aux] = d | |
| } | |
| d.varDefs = append(d.varDefs, v) | |
| } | |
| } | |
| } | |
| // Compute the use pattern of variables and the memory access demographic patterns. | |
| for _, b := range f.Blocks { | |
| for _, v := range b.Values { | |
| if v.Op == ssaop.OpLocalAddr { | |
| if n := v.Aux.(*ir.Name); n.Class == ir.PAUTO || isABIInternalParam(f, n) { | |
| ut := classifyUses(v, u) | |
| d := demographics[n] | |
| if d == nil { | |
| d = &variableDemographic{ls: true, lszmco: true} | |
| demographics[n] = d | |
| } | |
| if ut&^(useLoad|useStore) != 0 { | |
| d.ls = false | |
| } | |
| if ut&useOther != 0 { | |
| d.lszmco = false | |
| } | |
| localAddrs = append(localAddrs, v) | |
| } | |
| } | |
| } | |
| } | |
| // Names that have a VarLive applied are expected by the runtime to stay | |
| // in memory (e.g. to be kept alive across a call for the GC), so they do | |
| // not qualify for promotion. | |
| for n := range varLives { | |
| if d := demographics[n]; d != nil { | |
| d.ls = false | |
| d.lszmco = false | |
| } | |
| } | |
| return | |
| } | |
| var reinterpretOpMap = map[[2]types.Kind]ssaop.Op{ | |
| {types.TUINT32, types.TFLOAT32}: ssaop.OpI32AsF32, | |
| {types.TINT32, types.TFLOAT32}: ssaop.OpI32AsF32, | |
| {types.TFLOAT32, types.TUINT32}: ssaop.OpF32AsI32, | |
| {types.TFLOAT32, types.TINT32}: ssaop.OpF32AsI32, | |
| {types.TUINT64, types.TFLOAT64}: ssaop.OpI64AsF64, | |
| {types.TINT64, types.TFLOAT64}: ssaop.OpI64AsF64, | |
| {types.TFLOAT64, types.TUINT64}: ssaop.OpF64AsI64, | |
| {types.TFLOAT64, types.TINT64}: ssaop.OpF64AsI64, | |
| } | |
| // reinterpretOp returns a t1 => t2 reinterpret op if available in [reinterpretOpMap]. | |
| // Otherwise it returns OpInvalid. | |
| func reinterpretOp(t1, t2 *types.Type) ssaop.Op { | |
| if buildcfg.GOARCH != "amd64" { | |
| // Currently only amd64 has the proper lowering rules for these ops. | |
| return ssaop.OpInvalid | |
| } | |
| if op, ok := reinterpretOpMap[[2]types.Kind{t1.Kind(), t2.Kind()}]; ok { | |
| return op | |
| } | |
| return ssaop.OpInvalid | |
| } | |
| // copyCompatibleType reports whether a value of type t1 can directly stand in | |
| // for a value of type t2 (same size and compatible representation). | |
| func copyCompatibleType(t1, t2 *types.Type) bool { | |
| if t1.Size() != t2.Size() { | |
| return false | |
| } | |
| if t1.IsInteger() { | |
| return t2.IsInteger() | |
| } | |
| if ssa.IsPtr(t1) { | |
| return ssa.IsPtr(t2) | |
| } | |
| return t1.Compare(t2) == types.CMPeq | |
| } | |
| // sortLoadStores sorts loadStores (Loads and Stores in block bb) into | |
| // program order, i.e. the order given by bb's store chain. This is needed | |
| // because there is no guarantee of their order in b.Values before the | |
| // schedule pass. | |
| // | |
| // memoryOrders is a per-block cache, populated on demand: | |
| // memoryOrders[b.ID][v.ID] is the position of the memory value v in b's | |
| // store chain, in even increments. Position 0 is the memory coming into | |
| // the block (InitMem, a memory Phi, or any memory defined outside bb); | |
| // each memory-producing value in bb (a Store, but also any other memory | |
| // generator such as a call) gets the position of its memory arg plus | |
| // two. A Load sorts at the position of its memory arg plus one - odd, | |
| // so after the value that produced that memory and before whatever | |
| // consumes it, which orders Stores before Loads of their output memory | |
| // with a plain integer comparison. | |
| func sortLoadStores(bb *ssa.Block, loadStores []*ssa.Value, memoryOrders map[ssa.ID]map[ssa.ID]int) []*ssa.Value { | |
| memOrder, ok := memoryOrders[bb.ID] | |
| if !ok { | |
| memOrder = make(map[ssa.ID]int) | |
| var computeDepth func(v *ssa.Value) int | |
| computeDepth = func(v *ssa.Value) int { | |
| if d, ok := memOrder[v.ID]; ok { | |
| return d | |
| } | |
| // Starting point | |
| if v.Block != bb || v.Op == ssaop.OpInitMem || v.Op == ssaop.OpPhi { | |
| memOrder[v.ID] = 0 | |
| return 0 | |
| } | |
| // Search backwards | |
| d := computeDepth(v.MemoryArg()) + 2 | |
| memOrder[v.ID] = d | |
| return d | |
| } | |
| for _, v := range bb.Values { | |
| if v.Type.IsMemory() { | |
| computeDepth(v) | |
| } | |
| } | |
| memoryOrders[bb.ID] = memOrder | |
| } | |
| key := func(v *ssa.Value) int { | |
| if v.Op == ssaop.OpLoad { | |
| return memOrder[v.MemoryArg().ID] + 1 | |
| } | |
| return memOrder[v.ID] | |
| } | |
| slices.SortFunc(loadStores, func(a, b *ssa.Value) int { | |
| return key(a) - key(b) | |
| }) | |
| return loadStores | |
| } | |
| func mem2reg(f *ssa.Func) { | |
| changed := false | |
| if base.Flag.N != 0 { | |
| return | |
| } | |
| st := f.NewStats("mem2reg") | |
| // Get demographics | |
| demographics, localAddrs, uses := variableDemographics(f) | |
| if uses == nil { | |
| // No candidate variables; nothing to do. | |
| return | |
| } | |
| defer uses.free(f) | |
| memoryOrders := make(map[ssa.ID]map[ssa.ID]int) | |
| // First, we need to group all LocalAddr/Addrs that point to the same variable name | |
| // varGrouped[n] = all localAddrs that have n as their Aux field and have only load and stores. | |
| varGrouped := make(map[*ir.Name][]*ssa.Value) | |
| for _, v := range localAddrs { | |
| // TODO: mem2reg currently only supports load and stores on the LocalAddr directly. | |
| // However as a small step further it could also handle copy-derived addresses: | |
| // OffPtr [0] LocalAddr | |
| // Copy LocalAddr | |
| if d, ok := demographics[v.Aux]; !ok || !d.ls { | |
| continue | |
| } | |
| n := v.Aux.(*ir.Name) | |
| if n.Class != ir.PAUTO { | |
| // Not handling params right now. | |
| continue | |
| } | |
| varGrouped[n] = append(varGrouped[n], v) | |
| } | |
| namesOrdered := []*ir.Name{} | |
| for n, vag := range varGrouped { | |
| namesOrdered = append(namesOrdered, n) | |
| slices.SortFunc(vag, func(a, b *ssa.Value) int { return int(a.ID - b.ID) }) | |
| } | |
| slices.SortFunc(namesOrdered, func(a, b *ir.Name) int { | |
| return int(varGrouped[a][0].ID - varGrouped[b][0].ID) | |
| }) | |
| // Utility functions | |
| removeStore := func(v *ssa.Value) { | |
| changed = true | |
| v.SetArgs1(v.MemoryArg()) | |
| v.Aux = nil | |
| v.AuxInt = 0 | |
| v.Op = ssaop.OpCopy | |
| } | |
| type loadCandidate struct { | |
| l *ssa.Value // The load | |
| v *ssa.Value // The value to replace the load | |
| // if v needs to be reinterpreted to match l's type, the op required. OpInvalid otherwise. | |
| // if l.Type != v.Type, this op must not be OpInvalid. | |
| reinterpret ssaop.Op | |
| } | |
| replaceLoad := func(lc loadCandidate) { | |
| changed = true | |
| if lc.reinterpret == ssaop.OpInvalid { | |
| if !copyCompatibleType(lc.l.Type, lc.v.Type) { | |
| f.Fatalf("mem2reg: load is being replaced by a value of an incompatible type") | |
| } | |
| lc.l.SetArgs1(lc.v) | |
| } else { | |
| // TODO: if we want to be more cautious, check that the reinterpret op will | |
| // actually make the type right. | |
| lc.l.SetArgs1(lc.l.Block.NewValue1(lc.l.Pos, lc.reinterpret, lc.l.Type, lc.v)) | |
| } | |
| lc.l.Aux = nil | |
| lc.l.AuxInt = 0 | |
| lc.l.Op = ssaop.OpCopy | |
| } | |
| // Now we should start the promotion | |
| // Simple case one - all uses of v is within the same block. | |
| storeCands := []*ssa.Value{} // These stores are to be overwritten | |
| loadCands := []loadCandidate{} | |
| vaUses := []*ssa.Value{} | |
| NextVar: | |
| for _, n := range namesOrdered { | |
| vag := varGrouped[n] | |
| var block *ssa.Block | |
| for _, va := range vag { | |
| // Walk through all uses of v and check if they are within the same block | |
| for _, use := range uses.of(va) { | |
| if block == nil { | |
| block = use.Block | |
| } | |
| if use.Block != block { | |
| // Across control flow, needs DF and Phi. | |
| continue NextVar | |
| } | |
| } | |
| } | |
| // The uses are all within the same block and are loads/stores. | |
| storeCands = storeCands[:0] | |
| loadCands = loadCands[:0] | |
| vaUses = vaUses[:0] | |
| for _, va := range vag { | |
| vaUses = append(vaUses, uses.of(va)...) | |
| } | |
| vaUses = sortLoadStores(block, vaUses, memoryOrders) | |
| var curV *ssa.Value // The current value of n. | |
| for _, v := range vaUses { | |
| switch v.Op { | |
| case ssaop.OpLoad: | |
| if curV == nil { | |
| f.Fatalf("mem2reg sees a load from an auto variable before any store") | |
| } | |
| reinter := ssaop.OpInvalid | |
| if !copyCompatibleType(v.Type, curV.Type) { | |
| reinter = reinterpretOp(curV.Type, v.Type) | |
| if reinter == ssaop.OpInvalid { | |
| // Not something we can optimize, bailout the variable and | |
| // continue to the next variable. | |
| st.Record("incompatible types in single block case", 1) | |
| delete(varGrouped, n) | |
| continue NextVar | |
| } | |
| } | |
| loadCands = append(loadCands, loadCandidate{ | |
| l: v, | |
| v: curV, | |
| reinterpret: reinter, | |
| }) | |
| case ssaop.OpStore: | |
| curV = v.Args[1] | |
| storeCands = append(storeCands, v) | |
| default: | |
| f.Fatalf("should only has load or store uses") | |
| } | |
| } | |
| // Do the replacement. | |
| for _, v := range storeCands { | |
| // Remove the stores. | |
| removeStore(v) | |
| } | |
| for _, v := range demographics[n].varDefs { | |
| // Also remove the VarDefs. | |
| removeStore(v) | |
| } | |
| for _, lc := range loadCands { | |
| // Replace the loads with the corresponding value. | |
| replaceLoad(lc) | |
| } | |
| // This var is done, remove it from varGrouped. | |
| if f.Pass.Debug > 1 { | |
| f.Warnl(n.Pos(), "promoted %v in single block case", n) | |
| } | |
| delete(varGrouped, n) | |
| st.Record("promoted variable in single block case", 1) | |
| } | |
| // TODO: the rest of variables needs analysis across basic blocks, implement this. | |
| if changed { | |
| deadcode(f) | |
| } | |
| } |