// Copyright 2023 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 import ( "cmp" "slices" "cmd/compile/internal/base" "cmd/compile/internal/ssa" "cmd/compile/internal/ssa/ssaop" "cmd/compile/internal/types" "cmd/internal/src" ) // memcombine combines smaller loads and stores into larger ones. // This produces good code for encoding/binary operations and may help other // cases too. On architectures that do not allow unaligned accesses, the pass // uses pointer alignment facts to avoid introducing unaligned wider operations. func memcombine(f *ssa.Func) { var ptrAlignments []int8 if !f.Config.UnalignedOK { ptrAlignments = f.Cache.AllocInt8Slice(f.NumValues()) defer f.Cache.FreeInt8Slice(ptrAlignments) computePtrAlignments(f, ptrAlignments) } memcombineLoads(f, ptrAlignments) memcombineStores(f, ptrAlignments) } func memcombineLoads(f *ssa.Func, ptrAlignments []int8) { // Find "OR trees" to start with. mark := f.NewSparseSet(f.NumValues()) defer f.RetSparseSet(mark) var order []*ssa.Value // Mark all values that are the argument of an OR. for _, b := range f.Blocks { for _, v := range b.Values { if v.Op == ssaop.OpOr16 || v.Op == ssaop.OpOr32 || v.Op == ssaop.OpOr64 { mark.Add(v.Args[0].ID) mark.Add(v.Args[1].ID) } } } for _, b := range f.Blocks { order = order[:0] for _, v := range b.Values { if v.Op != ssaop.OpOr16 && v.Op != ssaop.OpOr32 && v.Op != ssaop.OpOr64 { continue } if mark.Contains(v.ID) { // marked - means it is not the root of an OR tree continue } // Add the OR tree rooted at v to the order. // We use BFS here, but any walk that puts roots before leaves would work. i := len(order) order = append(order, v) for ; i < len(order); i++ { x := order[i] for j := 0; j < 2; j++ { a := x.Args[j] if a.Op == ssaop.OpOr16 || a.Op == ssaop.OpOr32 || a.Op == ssaop.OpOr64 { order = append(order, a) } } } } for _, v := range order { max := f.Config.RegSize switch v.Op { case ssaop.OpOr64: case ssaop.OpOr32: max = 4 case ssaop.OpOr16: max = 2 default: continue } for n := max; n > 1; n /= 2 { if combineLoads(v, n, ptrAlignments) { break } } } } } // A BaseAddress represents the address ptr+idx, where // ptr is a pointer type and idx is an integer type. // idx may be nil, in which case it is treated as 0. type BaseAddress struct { ptr *ssa.Value idx Index } // Index represents an address index in the form exp< 0 { return int64(align) } return 1 } // valuePtrAlignment computes one entry in ptrAlignments. func valuePtrAlignment(v *ssa.Value, ptrAlignments []int8) int64 { // computePtrAlignments visits every SSA value, not just pointer values. if !v.Type.IsPtr() { return 1 } switch v.Op { case ssaop.OpOffPtr: return offsetAlignment(ptrAlignment(v.Args[0], ptrAlignments), v.AuxInt) case ssaop.OpCopy, ssaop.OpNilCheck: return ptrAlignment(v.Args[0], ptrAlignments) case ssaop.OpAddr, ssaop.OpLocalAddr, ssaop.OpArg, ssaop.OpArgIntReg: return typeAlignment(v.Type.Elem()) case ssaop.OpPhi: align := ptrAlignment(v.Args[0], ptrAlignments) for _, arg := range v.Args[1:] { if argAlign := ptrAlignment(arg, ptrAlignments); argAlign < align { align = argAlign } } return align } return 1 } // typeAlignment returns a conservative alignment for t without calling // Type.Alignment, which may try to calculate type sizes while the compiler // back end is running concurrently. func typeAlignment(t *types.Type) int64 { switch t.Kind() { case types.TBOOL, types.TINT8, types.TUINT8: return 1 case types.TINT16, types.TUINT16: return 2 case types.TINT32, types.TUINT32, types.TFLOAT32, types.TCOMPLEX64: return 4 case types.TINT64, types.TUINT64, types.TFLOAT64, types.TCOMPLEX128: return 8 case types.TINT, types.TUINT, types.TUINTPTR, types.TPTR, types.TUNSAFEPTR, types.TSTRING, types.TSLICE, types.TFUNC, types.TMAP, types.TCHAN: return int64(types.PtrSize) case types.TARRAY: return typeAlignment(t.Elem()) case types.TSTRUCT: align := int64(1) for _, f := range t.Fields() { fieldAlign := typeAlignment(f.Type) if fieldAlign > align { align = fieldAlign } } return align } return 1 } func offsetAlignment(align, off int64) int64 { off &= align - 1 if off == 0 { return align } return off & -off } func combineLoads(root *ssa.Value, n int64, ptrAlignments []int8) bool { orOp := root.Op var shiftOp ssaop.Op switch orOp { case ssaop.OpOr64: shiftOp = ssaop.OpLsh64x64 case ssaop.OpOr32: shiftOp = ssaop.OpLsh32x64 case ssaop.OpOr16: shiftOp = ssaop.OpLsh16x64 default: return false } // Find n values that are ORed together with the above op. a := make([]*ssa.Value, 0, 8) a = append(a, root) for i := 0; i < len(a) && int64(len(a)) < n; i++ { v := a[i] if v.Uses != 1 && v != root { // Something in this subtree is used somewhere else. return false } if v.Op == orOp { a[i] = v.Args[0] a = append(a, v.Args[1]) i-- } } if int64(len(a)) != n { return false } // Check that the first entry to see what ops we're looking for. // All the entries should be of the form shift(extend(load)), maybe with no shift. v := a[0] if v.Op == shiftOp { v = v.Args[0] } var extOp ssaop.Op if orOp == ssaop.OpOr64 && (v.Op == ssaop.OpZeroExt8to64 || v.Op == ssaop.OpZeroExt16to64 || v.Op == ssaop.OpZeroExt32to64) || orOp == ssaop.OpOr32 && (v.Op == ssaop.OpZeroExt8to32 || v.Op == ssaop.OpZeroExt16to32) || orOp == ssaop.OpOr16 && v.Op == ssaop.OpZeroExt8to16 { extOp = v.Op v = v.Args[0] } else { return false } if v.Op != ssaop.OpLoad { return false } base, _ := splitPtr(v.Args[0]) mem := v.Args[1] size := v.Type.Size() if root.Block.Func.Config.Arch == "S390X" { // s390x can't handle unaligned accesses to global variables. if base.ptr.Op == ssaop.OpAddr { return false } } // Check all the entries, extract useful info. type LoadRecord struct { load *ssa.Value offset int64 // offset of load address from base shift int64 } r := make([]LoadRecord, n, 8) for i := int64(0); i < n; i++ { v := a[i] if v.Uses != 1 { return false } shift := int64(0) if v.Op == shiftOp { v, shift = peelShift(v) if v.Uses != 1 { return false } } if v.Op != extOp { return false } load := v.Args[0] if load.Op != ssaop.OpLoad { return false } if load.Uses != 1 { return false } if load.Args[1] != mem { return false } p, off := splitPtr(load.Args[0]) if p != base { return false } r[i] = LoadRecord{load: load, offset: off, shift: shift} } // Sort in memory address order. slices.SortFunc(r, func(a, b LoadRecord) int { return cmp.Compare(a.offset, b.offset) }) // Check that we have contiguous offsets. for i := int64(0); i < n; i++ { if r[i].offset != r[0].offset+i*size { return false } } if !root.Block.Func.Config.UnalignedOK && ptrAlignment(r[0].load.Args[0], ptrAlignments) < n*size { return false } // Check for reads in little-endian or big-endian order. shift0 := r[0].shift isLittleEndian := true for i := int64(0); i < n; i++ { if r[i].shift != shift0+i*size*8 { isLittleEndian = false break } } isBigEndian := true for i := int64(0); i < n; i++ { if r[i].shift != shift0-i*size*8 { isBigEndian = false break } } if !isLittleEndian && !isBigEndian { return false } // Find a place to put the new load. // This is tricky, because it has to be at a point where // its memory argument is live. We can't just put it in root.Block. // We use the block of the latest load. loads := make([]*ssa.Value, n, 8) for i := int64(0); i < n; i++ { loads[i] = r[i].load } loadBlock := mergePoint(root.Block, loads...) if loadBlock == nil { return false } // Find a source position to use. pos := src.NoXPos for _, load := range loads { if load.Block == loadBlock { pos = load.Pos break } } if pos == src.NoXPos { return false } // Check to see if we need byte swap before storing. needSwap := isLittleEndian && root.Block.Func.Config.BigEndian || isBigEndian && !root.Block.Func.Config.BigEndian if needSwap && (size != 1 || !root.Block.Func.Config.HaveByteSwap(n)) { return false } // This is the commit point. // First, issue load at lowest address. v = loadBlock.NewValue2(pos, ssaop.OpLoad, sizeType(n*size), r[0].load.Args[0], mem) // Byte swap if needed, if needSwap { v = byteSwap(loadBlock, pos, v) } // Extend if needed. if n*size < root.Type.Size() { v = zeroExtend(loadBlock, pos, v, n*size, root.Type.Size()) } // Shift if needed. if isLittleEndian && shift0 != 0 { v = leftShift(loadBlock, pos, v, shift0) } if isBigEndian && shift0-(n-1)*size*8 != 0 { v = leftShift(loadBlock, pos, v, shift0-(n-1)*size*8) } // Install with (Copy v). root.Reset(ssaop.OpCopy) root.AddArg(v) // Clobber the loads, just to prevent additional work being done on // subtrees (which are now unreachable). for i := int64(0); i < n; i++ { ssa.Clobber(r[i].load) } return true } func memcombineStores(f *ssa.Func, ptrAlignments []int8) { mark := f.NewSparseSet(f.NumValues()) defer f.RetSparseSet(mark) var order []*ssa.Value for _, b := range f.Blocks { // Mark all stores which are not last in a store sequence. mark.Clear() for _, v := range b.Values { if v.Op == ssaop.OpStore { mark.Add(v.MemoryArg().ID) } } // pick an order for visiting stores such that // later stores come earlier in the ordering. order = order[:0] for _, v := range b.Values { if v.Op != ssaop.OpStore { continue } if mark.Contains(v.ID) { continue // not last in a chain of stores } for { order = append(order, v) v = v.Args[2] if v.Block != b || v.Op != ssaop.OpStore { break } } } // Look for combining opportunities at each store in queue order. for _, v := range order { if v.Op != ssaop.OpStore { // already rewritten continue } size := v.Aux.(*types.Type).Size() if size >= f.Config.RegSize || size == 0 { continue } combineStores(v, ptrAlignments) } } } // combineStores tries to combine the stores ending in root. func combineStores(root *ssa.Value, ptrAlignments []int8) { // Helper functions. maxRegSize := root.Block.Func.Config.RegSize type StoreRecord struct { store *ssa.Value offset int64 size int64 } getShiftBase := func(a []StoreRecord) *ssa.Value { x := a[0].store.Args[1] y := a[1].store.Args[1] switch x.Op { case ssaop.OpTrunc64to8, ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc16to8: x = x.Args[0] default: return nil } switch y.Op { case ssaop.OpTrunc64to8, ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc16to8: y = y.Args[0] default: return nil } var x2 *ssa.Value switch x.Op { case ssaop.OpRsh64Ux64, ssaop.OpRsh32Ux64, ssaop.OpRsh16Ux64: x2 = x.Args[0] default: } var y2 *ssa.Value switch y.Op { case ssaop.OpRsh64Ux64, ssaop.OpRsh32Ux64, ssaop.OpRsh16Ux64: y2 = y.Args[0] default: } if y2 == x { // a shift of x and x itself. return x } if x2 == y { // a shift of y and y itself. return y } if x2 == y2 { // 2 shifts both of the same argument. return x2 } return nil } isShiftBase := func(v, base *ssa.Value) bool { val := v.Args[1] switch val.Op { case ssaop.OpTrunc64to8, ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc16to8: val = val.Args[0] default: return false } if val == base { return true } switch val.Op { case ssaop.OpRsh64Ux64, ssaop.OpRsh32Ux64, ssaop.OpRsh16Ux64: val = val.Args[0] default: return false } return val == base } shift := func(v, base *ssa.Value) int64 { val := v.Args[1] switch val.Op { case ssaop.OpTrunc64to8, ssaop.OpTrunc64to16, ssaop.OpTrunc64to32, ssaop.OpTrunc32to8, ssaop.OpTrunc32to16, ssaop.OpTrunc16to8: val = val.Args[0] default: return -1 } if val == base { return 0 } switch val.Op { case ssaop.OpRsh64Ux64, ssaop.OpRsh32Ux64, ssaop.OpRsh16Ux64: val = val.Args[1] default: return -1 } if val.Op != ssaop.OpConst64 { return -1 } return val.AuxInt } // Gather n stores to look at. Check easy conditions we require. allMergeable := make([]StoreRecord, 0, 8) rbase, roff := splitPtr(root.Args[0]) if root.Block.Func.Config.Arch == "S390X" { // s390x can't handle unaligned accesses to global variables. if rbase.ptr.Op == ssaop.OpAddr { return } } allMergeable = append(allMergeable, StoreRecord{root, roff, root.Aux.(*types.Type).Size()}) allMergeableSize := root.Aux.(*types.Type).Size() // TODO: this loop strictly requires stores to chain together in memory. // maybe we can break this constraint and match more patterns. for i, x := 1, root.Args[2]; i < 8; i, x = i+1, x.Args[2] { if x.Op != ssaop.OpStore { break } if x.Block != root.Block { break } if x.Uses != 1 { // Note: root can have more than one use. break } xSize := x.Aux.(*types.Type).Size() if xSize == 0 { break } if xSize > maxRegSize-allMergeableSize { break } base, off := splitPtr(x.Args[0]) if base != rbase { break } allMergeable = append(allMergeable, StoreRecord{x, off, xSize}) allMergeableSize += xSize } if len(allMergeable) <= 1 { return } // Fit the combined total size to be one of the register size. mergeableSet := map[int64][]StoreRecord{} for i, size := 0, int64(0); i < len(allMergeable); i++ { size += allMergeable[i].size for _, bucketSize := range []int64{8, 4, 2} { if size == bucketSize { mergeableSet[size] = slices.Clone(allMergeable[:i+1]) break } } } var a []StoreRecord var aTotalSize int64 var mem *ssa.Value var pos src.XPos // Pick the largest mergeable set. for _, s := range []int64{8, 4, 2} { candidate := mergeableSet[s] // TODO: a refactoring might be more efficient: // Find a bunch of stores that are all adjacent and then decide how big a chunk of // those sequential stores to combine. if len(candidate) >= 2 { // Before we sort, grab the memory arg the result should have. mem = candidate[len(candidate)-1].store.Args[2] // Also grab position of first store (last in array = first in memory order). pos = candidate[len(candidate)-1].store.Pos // Sort stores in increasing address order. slices.SortFunc(candidate, func(sr1, sr2 StoreRecord) int { return cmp.Compare(sr1.offset, sr2.offset) }) // Check that everything is written to sequential locations. sequential := true for i := 1; i < len(candidate); i++ { if candidate[i].offset != candidate[i-1].offset+candidate[i-1].size { sequential = false break } } if sequential { a = candidate aTotalSize = s break } } } if len(a) <= 1 { return } // Memory location we're going to write at (the lowest one). ptr := a[0].store.Args[0] if !root.Block.Func.Config.UnalignedOK && ptrAlignment(ptr, ptrAlignments) < aTotalSize { return } // Check for constant stores isConst := true for i := range a { switch a[i].store.Args[1].Op { case ssaop.OpConst32, ssaop.OpConst16, ssaop.OpConst8, ssaop.OpConstBool: default: isConst = false } if !isConst { break } } if isConst { // Modify root to do all the stores. var c int64 for i := range a { mask := int64(1)<<(8*a[i].size) - 1 s := 8 * (a[i].offset - a[0].offset) if root.Block.Func.Config.BigEndian { s = (aTotalSize-a[i].size)*8 - s } c |= (a[i].store.Args[1].AuxInt & mask) << s } var cv *ssa.Value switch aTotalSize { case 2: cv = root.Block.Func.ConstInt16(types.Types[types.TUINT16], int16(c)) case 4: cv = root.Block.Func.ConstInt32(types.Types[types.TUINT32], int32(c)) case 8: cv = root.Block.Func.ConstInt64(types.Types[types.TUINT64], c) } // Move all the stores to the root. for i := range a { v := a[i].store if v == root { v.Aux = cv.Type // widen store type v.Pos = pos v.SetArg(0, ptr) v.SetArg(1, cv) v.SetArg(2, mem) } else { ssa.Clobber(v) v.Type = types.Types[types.TBOOL] // erase memory type } } return } // Check for consecutive loads as the source of the stores. var loadMem *ssa.Value var loadBase BaseAddress var loadIdx int64 for i := range a { load := a[i].store.Args[1] if load.Op != ssaop.OpLoad { loadMem = nil break } if load.Uses != 1 { loadMem = nil break } if load.Type.HasPointers() { // Don't combine stores containing a pointer, as we need // a write barrier for those. This can happen on an // 8-byte-reg/4-byte-ptr architecture like wasm32. loadMem = nil break } mem := load.Args[1] base, idx := splitPtr(load.Args[0]) if loadMem == nil { // First one we found loadMem = mem loadBase = base loadIdx = idx continue } if base != loadBase || mem != loadMem { loadMem = nil break } if idx != loadIdx+(a[i].offset-a[0].offset) { loadMem = nil break } } if loadMem != nil { // Modify the first load to do a larger load instead. load := a[0].store.Args[1] if !root.Block.Func.Config.UnalignedOK && ptrAlignment(load.Args[0], ptrAlignments) < aTotalSize { return } switch aTotalSize { case 2: load.Type = types.Types[types.TUINT16] case 4: load.Type = types.Types[types.TUINT32] case 8: load.Type = types.Types[types.TUINT64] } // Modify root to do the store. for i := range a { v := a[i].store if v == root { v.Aux = load.Type // widen store type v.Pos = pos v.SetArg(0, ptr) v.SetArg(1, load) v.SetArg(2, mem) } else { ssa.Clobber(v) v.Type = types.Types[types.TBOOL] // erase memory type } } return } // Check that all the shift/trunc are of the same base value. shiftBase := getShiftBase(a) if shiftBase == nil { return } for i := range a { if !isShiftBase(a[i].store, shiftBase) { return } } // Check for writes in little-endian or big-endian order. isLittleEndian := true shift0 := shift(a[0].store, shiftBase) for i := 1; i < len(a); i++ { if shift(a[i].store, shiftBase) != shift0+(a[i].offset-a[0].offset)*8 { isLittleEndian = false break } } isBigEndian := true shiftedSize := int64(0) for i := 1; i < len(a); i++ { shiftedSize += a[i].size if shift(a[i].store, shiftBase) != shift0-shiftedSize*8 { isBigEndian = false break } } if !isLittleEndian && !isBigEndian { return } // Check to see if we need byte swap before storing. needSwap := isLittleEndian && root.Block.Func.Config.BigEndian || isBigEndian && !root.Block.Func.Config.BigEndian if needSwap && (int64(len(a)) != aTotalSize || !root.Block.Func.Config.HaveByteSwap(aTotalSize)) { return } // This is the commit point. // Modify root to do all the stores. sv := shiftBase if isLittleEndian && shift0 != 0 { sv = rightShift(root.Block, root.Pos, sv, shift0) } shiftedSize = aTotalSize - a[0].size if isBigEndian && shift0-shiftedSize*8 != 0 { sv = rightShift(root.Block, root.Pos, sv, shift0-shiftedSize*8) } if sv.Type.Size() > aTotalSize { sv = truncate(root.Block, root.Pos, sv, sv.Type.Size(), aTotalSize) } if needSwap { sv = byteSwap(root.Block, root.Pos, sv) } // Move all the stores to the root. for i := range a { v := a[i].store if v == root { v.Aux = sv.Type // widen store type v.Pos = pos v.SetArg(0, ptr) v.SetArg(1, sv) v.SetArg(2, mem) } else { ssa.Clobber(v) v.Type = types.Types[types.TBOOL] // erase memory type } } } func sizeType(size int64) *types.Type { switch size { case 8: return types.Types[types.TUINT64] case 4: return types.Types[types.TUINT32] case 2: return types.Types[types.TUINT16] default: base.Fatalf("bad size %d\n", size) return nil } } func truncate(b *ssa.Block, pos src.XPos, v *ssa.Value, from, to int64) *ssa.Value { switch from*10 + to { case 82: return b.NewValue1(pos, ssaop.OpTrunc64to16, types.Types[types.TUINT16], v) case 84: return b.NewValue1(pos, ssaop.OpTrunc64to32, types.Types[types.TUINT32], v) case 42: return b.NewValue1(pos, ssaop.OpTrunc32to16, types.Types[types.TUINT16], v) default: base.Fatalf("bad sizes %d %d\n", from, to) return nil } } func zeroExtend(b *ssa.Block, pos src.XPos, v *ssa.Value, from, to int64) *ssa.Value { switch from*10 + to { case 24: return b.NewValue1(pos, ssaop.OpZeroExt16to32, types.Types[types.TUINT32], v) case 28: return b.NewValue1(pos, ssaop.OpZeroExt16to64, types.Types[types.TUINT64], v) case 48: return b.NewValue1(pos, ssaop.OpZeroExt32to64, types.Types[types.TUINT64], v) default: base.Fatalf("bad sizes %d %d\n", from, to) return nil } } func leftShift(b *ssa.Block, pos src.XPos, v *ssa.Value, shift int64) *ssa.Value { s := b.Func.ConstInt64(types.Types[types.TUINT64], shift) size := v.Type.Size() switch size { case 8: return b.NewValue2(pos, ssaop.OpLsh64x64, v.Type, v, s) case 4: return b.NewValue2(pos, ssaop.OpLsh32x64, v.Type, v, s) case 2: return b.NewValue2(pos, ssaop.OpLsh16x64, v.Type, v, s) default: base.Fatalf("bad size %d\n", size) return nil } } func rightShift(b *ssa.Block, pos src.XPos, v *ssa.Value, shift int64) *ssa.Value { s := b.Func.ConstInt64(types.Types[types.TUINT64], shift) size := v.Type.Size() switch size { case 8: return b.NewValue2(pos, ssaop.OpRsh64Ux64, v.Type, v, s) case 4: return b.NewValue2(pos, ssaop.OpRsh32Ux64, v.Type, v, s) case 2: return b.NewValue2(pos, ssaop.OpRsh16Ux64, v.Type, v, s) default: base.Fatalf("bad size %d\n", size) return nil } } func byteSwap(b *ssa.Block, pos src.XPos, v *ssa.Value) *ssa.Value { switch v.Type.Size() { case 8: return b.NewValue1(pos, ssaop.OpBswap64, v.Type, v) case 4: return b.NewValue1(pos, ssaop.OpBswap32, v.Type, v) case 2: return b.NewValue1(pos, ssaop.OpBswap16, v.Type, v) default: v.Fatalf("bad size %d\n", v.Type.Size()) return nil } }