diff --git a/src/cmd/compile/internal/ssacompile/memcombine.go b/src/cmd/compile/internal/ssacompile/memcombine.go index 33898ff0098ddf..0ebd8f539ba85f 100644 --- a/src/cmd/compile/internal/ssacompile/memcombine.go +++ b/src/cmd/compile/internal/ssacompile/memcombine.go @@ -51,8 +51,8 @@ func memcombineLoads(f *ssa.Func, ptrAlignments []int8) { 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 + if v.Uses == 1 && mark.Contains(v.ID) { + // A single-use argument belongs to its parent's OR tree. continue } // Add the OR tree rooted at v to the order. @@ -63,7 +63,8 @@ func memcombineLoads(f *ssa.Func, ptrAlignments []int8) { 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 { + // combineLoads cannot combine through shared values. + if a.Uses == 1 && (a.Op == ssaop.OpOr16 || a.Op == ssaop.OpOr32 || a.Op == ssaop.OpOr64) { order = append(order, a) } } diff --git a/src/cmd/compile/internal/ssacompile/memcombine_test.go b/src/cmd/compile/internal/ssacompile/memcombine_test.go new file mode 100644 index 00000000000000..20b9d639fe3e1f --- /dev/null +++ b/src/cmd/compile/internal/ssacompile/memcombine_test.go @@ -0,0 +1,41 @@ +// 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 + +import ( + "fmt" + "testing" + + "cmd/compile/internal/ssa" + "cmd/compile/internal/ssa/block" + "cmd/compile/internal/ssa/ssaop" + "cmd/internal/src" +) + +func BenchmarkMemcombineSharedOR(b *testing.B) { + for _, n := range []int{8, 16, 22} { + b.Run(fmt.Sprint(n), func(b *testing.B) { + c := testConfig(b) + f := c.config.NewFunc(c.Frontend(), new(ssa.Cache)) + entry := f.NewBlock(block.BlockExit) + f.Entry = entry + typ := c.config.Types.UInt64 + x := entry.NewValue0(src.NoXPos, ssaop.OpArg, typ) + y := entry.NewValue0(src.NoXPos, ssaop.OpArg, typ) + v := entry.NewValue2(src.NoXPos, ssaop.OpOr64, typ, x, y) + // Each level uses the previous result through two parents. + // Expanding this graph as a tree takes exponential work. + for range n { + a := entry.NewValue2(src.NoXPos, ssaop.OpOr64, typ, v, x) + b := entry.NewValue2(src.NoXPos, ssaop.OpOr64, typ, v, y) + v = entry.NewValue2(src.NoXPos, ssaop.OpOr64, typ, a, b) + } + b.ReportAllocs() + for b.Loop() { + memcombineLoads(f, nil) + } + }) + } +} diff --git a/test/codegen/memcombine.go b/test/codegen/memcombine.go index 9e863aab5ce0d2..01404081cfcb9d 100644 --- a/test/codegen/memcombine.go +++ b/test/codegen/memcombine.go @@ -68,6 +68,14 @@ func load_le16(b []byte) uint16 { return binary.LittleEndian.Uint16(b) } +// A combined load that is shared by two OR expressions must still be found. +func load_le16_shared(b []byte, x, y uint16) (uint16, uint16) { + // amd64:`MOVWLZX \(.*\),` -`MOVB` + // arm64:`MOVHU \(R[0-9]+\),` -`MOVB` + v := uint16(b[0]) | uint16(b[1])<<8 + return v | x, v | y +} + func load_le16_idx(b []byte, idx int) uint16 { // amd64:`MOVWLZX \(.*\),` -`MOVB` -`OR` // ppc64le:`MOVHZ ` -`MOVBZ`