// Copyright 2016 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 ( "slices" "cmd/compile/internal/ir" "cmd/compile/internal/ssa" "cmd/compile/internal/ssa/block" "cmd/compile/internal/ssa/ssaop" "cmd/compile/internal/types" "cmd/internal/obj" ) // The pair pass finds memory operations that can be paired up // into single 2-register memory instructions. func pair(f *ssa.Func) { // Only arm64 for now. This pass is fairly arch-specific. switch f.Config.Arch { case "arm64": default: return } pairLoads(f) pairStores(f) } type pairInfo struct { width int64 // width of one element in the pair, in bytes pair ssaop.Op } // All pairableLoad ops must take 2 arguments, a pointer and a memory. // They must also take an offset in Aux/AuxInt. var pairableLoads = map[ssaop.Op]pairInfo{ ssaop.OpARM64MOVDload: {8, ssaop.OpARM64LDP}, ssaop.OpARM64MOVWUload: {4, ssaop.OpARM64LDPW}, ssaop.OpARM64MOVWload: {4, ssaop.OpARM64LDPSW}, // TODO: conceivably we could pair a signed and unsigned load // if we knew the upper bits of one of them weren't being used. ssaop.OpARM64FMOVDload: {8, ssaop.OpARM64FLDPD}, ssaop.OpARM64FMOVSload: {4, ssaop.OpARM64FLDPS}, // NEON loads ssaop.OpARM64FMOVQload: {16, ssaop.OpARM64FLDPQ}, } // All pairableStore keys must take 3 arguments, a pointer, a value, and a memory. // All pairableStore values must take 4 arguments, a pointer, 2 values, and a memory. // They must also take an offset in Aux/AuxInt. var pairableStores = map[ssaop.Op]pairInfo{ ssaop.OpARM64MOVDstore: {8, ssaop.OpARM64STP}, ssaop.OpARM64MOVWstore: {4, ssaop.OpARM64STPW}, ssaop.OpARM64FMOVDstore: {8, ssaop.OpARM64FSTPD}, ssaop.OpARM64FMOVSstore: {4, ssaop.OpARM64FSTPS}, // NEON stores ssaop.OpARM64FMOVQstore: {16, ssaop.OpARM64FSTPQ}, } // offsetOk returns true if a pair instruction should be used // for the offset Aux+off, when the data width (of the // unpaired instructions) is width. // This function is best-effort. The compiled function must // still work if offsetOk always returns true. // TODO: this is currently arm64-specific. func offsetOk(aux ssa.Aux, off, width int64) bool { if true { // Seems to generate slightly smaller code if we just // always allow this rewrite. // // Without pairing, we have 2 load instructions, like: // LDR 88(R0), R1 // LDR 96(R0), R2 // with pairing we have, best case: // LDP 88(R0), R1, R2 // but maybe we need an adjuster if out of range or unaligned: // ADD R0, $88, R27 // LDP (R27), R1, R2 // Even with the adjuster, it is at least no worse. // // A similar situation occurs when accessing globals. // Two loads from globals requires 4 instructions, // two ADRP and two LDR. With pairing, we need // ADRP+ADD+LDP, three instructions. // // With pairing, it looks like the critical path might // be a little bit longer. But it should never be more // instructions. // TODO: see if that longer critical path causes any // regressions. return true } if aux != nil { if _, ok := aux.(*ir.Name); !ok { // Offset is probably too big (globals). return false } // We let *ir.Names pass here, as // they are probably small offsets from SP. // There's no guarantee that we're in range // in that case though (we don't know the // stack frame size yet), so the assembler // might need to issue fixup instructions. // Assume some small frame size. if off >= 0 { off += 128 // This should be multiple of 4, 8 and 16 for later off%width check } // TODO: figure out how often this helps vs. hurts. } switch width { case 4, 8, 16: // Offset is encoded as signed 7 bit imm * width const simm7Min = -64 const simm7Max = 63 if off >= simm7Min*width && off <= simm7Max*width && off%width == 0 { return true } } return false } func pairLoads(f *ssa.Func) { var loads []*ssa.Value // Registry of aux values for sorting. auxIDs := map[ssa.Aux]int{} auxID := func(aux ssa.Aux) int { id, ok := auxIDs[aux] if !ok { id = len(auxIDs) auxIDs[aux] = id } return id } for _, b := range f.Blocks { // Find loads. loads = loads[:0] clear(auxIDs) for _, v := range b.Values { info := pairableLoads[v.Op] if info.width == 0 { continue // not pairable } if !offsetOk(v.Aux, v.AuxInt, info.width) { continue // not advisable } loads = append(loads, v) } if len(loads) < 2 { continue } // Sort to put pairable loads together. slices.SortFunc(loads, func(x, y *ssa.Value) int { // First sort by op, ptr, and memory arg. if x.Op != y.Op { return int(x.Op - y.Op) } if x.Args[0].ID != y.Args[0].ID { return int(x.Args[0].ID - y.Args[0].ID) } if x.Args[1].ID != y.Args[1].ID { return int(x.Args[1].ID - y.Args[1].ID) } // Then sort by aux. (nil first, then by aux ID) if x.Aux != nil { if y.Aux == nil { return 1 } a, b := auxID(x.Aux), auxID(y.Aux) if a != b { return a - b } } else if y.Aux != nil { return -1 } // Then sort by offset, low to high. return int(x.AuxInt - y.AuxInt) }) // Look for pairable loads. for i := 0; i < len(loads)-1; i++ { x := loads[i] y := loads[i+1] if x.Op != y.Op || x.Args[0] != y.Args[0] || x.Args[1] != y.Args[1] { continue } if x.Aux != y.Aux { continue } if x.AuxInt+pairableLoads[x.Op].width != y.AuxInt { continue } // Commit point. // Make the 2-register load. load := b.NewValue2IA(x.Pos, pairableLoads[x.Op].pair, types.NewTuple(x.Type, y.Type), x.AuxInt, x.Aux, x.Args[0], x.Args[1]) // Modify x to be (Select0 load). Similar for y. x.Reset(ssaop.OpSelect0) x.SetArgs1(load) y.Reset(ssaop.OpSelect1) y.SetArgs1(load) i++ // Skip y next time around the loop. } } // Try to pair a load with a load from a subsequent block. // Note that this is always safe to do if the memory arguments match. // (But see the memory barrier case below.) type nextBlockKey struct { op ssaop.Op ptr ssa.ID mem ssa.ID auxInt int64 aux any } nextBlock := map[nextBlockKey]*ssa.Value{} for _, b := range f.Blocks { if memoryBarrierTest(b) { // TODO: Do we really need to skip write barrier test blocks? // type T struct { // a *byte // b int // } // func f(t *T) int { // r := t.b // t.a = nil // return r // } // This would issue a single LDP for both the t.a and t.b fields, // *before* we check the write barrier flag. (We load the t.a field // to put it in the write barrier buffer.) Not sure if that is ok. continue } // Find loads in the next block(s) that we can move to this one. // TODO: could maybe look further than just one successor hop. clear(nextBlock) for _, e := range b.Succs { if len(e.B.Preds) > 1 { continue } for _, v := range e.B.Values { info := pairableLoads[v.Op] if info.width == 0 { continue } if !offsetOk(v.Aux, v.AuxInt, info.width) { continue // not advisable } nextBlock[nextBlockKey{op: v.Op, ptr: v.Args[0].ID, mem: v.Args[1].ID, auxInt: v.AuxInt, aux: v.Aux}] = v } } if len(nextBlock) == 0 { continue } // don't move too many loads. Each requires a register across a basic block boundary. const maxMoved = 4 nMoved := 0 for i := len(b.Values) - 1; i >= 0 && nMoved < maxMoved; i-- { x := b.Values[i] info := pairableLoads[x.Op] if info.width == 0 { continue } if !offsetOk(x.Aux, x.AuxInt, info.width) { continue // not advisable } key := nextBlockKey{op: x.Op, ptr: x.Args[0].ID, mem: x.Args[1].ID, auxInt: x.AuxInt + info.width, aux: x.Aux} if y := nextBlock[key]; y != nil { delete(nextBlock, key) // Make the 2-register load. load := b.NewValue2IA(x.Pos, info.pair, types.NewTuple(x.Type, y.Type), x.AuxInt, x.Aux, x.Args[0], x.Args[1]) // Modify x to be (Select0 load). x.Reset(ssaop.OpSelect0) x.SetArgs1(load) // Modify y to be (Copy (Select1 load)). // Note: the Select* needs to live in the load's block, not y's block. y.Reset(ssaop.OpCopy) y.SetArgs1(b.NewValue1(y.Pos, ssaop.OpSelect1, y.Type, load)) nMoved++ continue } key.auxInt = x.AuxInt - info.width if y := nextBlock[key]; y != nil { delete(nextBlock, key) // Make the 2-register load. load := b.NewValue2IA(x.Pos, info.pair, types.NewTuple(y.Type, x.Type), y.AuxInt, x.Aux, x.Args[0], x.Args[1]) // Modify x to be (Select1 load). x.Reset(ssaop.OpSelect1) x.SetArgs1(load) // Modify y to be (Copy (Select0 load)). y.Reset(ssaop.OpCopy) y.SetArgs1(b.NewValue1(y.Pos, ssaop.OpSelect0, y.Type, load)) nMoved++ continue } } } } func memoryBarrierTest(b *ssa.Block) bool { if b.Kind != block.BlockARM64NZW { return false } c := b.Controls[0] if c.Op != ssaop.OpARM64MOVWUload { return false } if globl, ok := c.Aux.(*obj.LSym); ok { return globl.Name == "runtime.writeBarrier" } return false } // pairStores merges store instructions. // It collects stores into a buffer where they can be freely reordered. // When encountering an instruction that cannot be added to the buffer, // it pairs the accumulated stores, flushes the buffer, and continues processing. func pairStores(f *ssa.Func) { last := f.Cache.AllocBoolSlice(f.NumValues()) defer f.Cache.FreeBoolSlice(last) // memChain contains a list of stores with the same ptr/aux pair and // nonoverlapping write ranges [AuxInt:AuxInt+writeSize]. All of the // elements of memChain can be reordered with each other. memChain := []*ssa.Value{} // Limit of length of memChain array. // This keeps us in O(n) territory. limit := 100 // flushMemChain sorts the stores in memChain and merges them when possible. // Then it flushes memChain. flushMemChain := func() { if len(memChain) < 2 { memChain = memChain[:0] return } // Sort in increasing AuxInt to put pairable stores together. slices.SortFunc(memChain, func(x, y *ssa.Value) int { return int(x.AuxInt - y.AuxInt) }) lastIdx := len(memChain) - 1 for i := 0; i < lastIdx; i++ { v := memChain[i] w := memChain[i+1] info := pairableStores[v.Op] off := v.AuxInt mem := v.MemoryArg() aux := v.Aux pos := v.Pos wmem := w.MemoryArg() if w.Op == v.Op && w.AuxInt == off+info.width { // Arguments for the merged store: ptr, val1, val2, mem. args := []*ssa.Value{v.Args[0], v.Args[1], w.Args[1], mem} v.Reset(info.pair) v.AddArgs(args...) v.Aux = aux v.AuxInt = off v.Pos = pos // Make w just a memory copy. w.Reset(ssaop.OpCopy) w.SetArgs1(wmem) // Skip merged store (w) i++ } } memChain = memChain[:0] } // prevStore returns the previous store in the // same block, or nil if there are none. prevStore := func(v *ssa.Value) *ssa.Value { if v.Op == ssaop.OpInitMem || v.Op == ssaop.OpPhi { return nil } m := v.MemoryArg() if m.Block != v.Block { return nil } return m } // storeWidth returns the width of store, // or 0 if it is not a store this pass understands. storeWidth := func(op ssaop.Op) int64 { if info, ok := pairableStores[op]; ok { return info.width } // We don't pair these stores, but returning zero here // would flush the memory chain. var width int64 switch op { case ssaop.OpARM64MOVHstore: width = 2 case ssaop.OpARM64MOVBstore: width = 1 default: width = 0 } return width } for _, b := range f.Blocks { memChain = memChain[:0] // Find last store in block, so we can // walk the stores last to first. // Last to first helps ensure that the rewrites we // perform do not get in the way of subsequent rewrites. for _, v := range b.Values { if v.Type.IsMemory() { last[v.ID] = true } } for _, v := range b.Values { if v.Type.IsMemory() { if m := prevStore(v); m != nil { last[m.ID] = false } } } var lastMem *ssa.Value for _, v := range b.Values { if last[v.ID] { lastMem = v break } } // Iterate over memory stores, accumulating them in memChain for potential merging. // Flush the chain when reordering is unsafe or a conflict is detected. for v := lastMem; v != nil; v = prevStore(v) { writeSize := storeWidth(v.Op) if writeSize == 0 { // We can't reorder stores with calls or other instructions // with writeSize == 0. flushMemChain() continue } if v.Uses != 1 && len(memChain) > 0 || len(memChain) > 0 && (v.Args[0] != memChain[0].Args[0] || v.Aux != memChain[0].Aux) || len(memChain) == limit { // 1. If v has multiple uses and it is not the latest store in the chain, // we cannot merge it with other store instructions. // // 2. If v has a different base pointer or Aux value from the current chain, // we need to flush memChain and start a new one with v. // // 3. If memChain length limit is exceeded, we also need to flush the chain // and start a new one with v. // // Only look back so far. // This keeps us in O(n) territory, and it // also prevents us from keeping values // in registers for too long (and thus // needing to spill them). flushMemChain() } for _, w := range memChain { wWriteSize := storeWidth(w.Op) if ssa.Overlap(w.AuxInt, wWriteSize, v.AuxInt, writeSize) { // Aliases with w's location. // Flush the chain and start a new one with v. flushMemChain() break } } memChain = append(memChain, v) } flushMemChain() } }