cmd/compile: preload multi-instruction constants before loops on arm64

The register allocator never preloads rematerializeable values into
registers before a loop, so a 64-bit constant that needs MOVD plus up
to three MOVKs on arm64 is rebuilt on every iteration. Preload such
constants when a register is free, as is already done for other
values live into the loop.

Single-instruction constants are unchanged.

Neoverse N1, repro from the issue (HashVar loads the constant from a
variable and is unaffected):

        │    base     │                 new                 │
        │   sec/op    │   sec/op     vs base                │
Hash      5.165µ ± 0%   4.128µ ± 0%  -20.09% (p=0.000 n=10)
HashVar   4.128µ ± 0%   4.129µ ± 0%        ~ (p=0.778 n=10)

For #81755

Change-Id: I87c5779b24057cd59833c2afc32c7ddef06a29df
Reviewed-on: https://go-review.googlesource.com/c/go/+/840405
Reviewed-by: Keith Randall <khr@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Keith Randall <khr@golang.org>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
diff --git a/src/cmd/compile/internal/ssacompile/regalloc.go b/src/cmd/compile/internal/ssacompile/regalloc.go
index 73af23f..47102c0 100644
--- a/src/cmd/compile/internal/ssacompile/regalloc.go
+++ b/src/cmd/compile/internal/ssacompile/regalloc.go
@@ -157,6 +157,25 @@
 	return bits.OnesCount64(r.V1) + bits.OnesCount64(r.V2)
 }
 
+// costlyRemat reports whether rematerializing v takes more than one
+// instruction.
+// TODO: logical immediates (bitcon) are also one instruction on arm64.
+func costlyRemat(v *ssa.Value) bool {
+	if v.Op != ssaop.OpARM64MOVDconst {
+		return false
+	}
+	zeros, ones := 0, 0
+	for i := 0; i < 64; i += 16 {
+		switch uint16(v.AuxInt >> i) {
+		case 0:
+			zeros++
+		case 0xffff:
+			ones++
+		}
+	}
+	return zeros < 3 && ones < 3
+}
+
 // pickReg picks a register from the register mask.
 func (s *regAllocState) pickReg(rm ssaop.RegMask) ssaop.Register {
 	if s.f.Config.Ctxt.Arch.Arch == sys.ArchRISCV64 {
@@ -2014,14 +2033,15 @@
 					if !vi.Regs.Empty() {
 						continue
 					}
-					if vi.Rematerializeable {
-						// TODO: maybe we should not skip rematerializeable
-						// values here. One rematerialization outside the loop
-						// is better than N in the loop. But rematerializations
-						// are cheap, and spilling another value may not be.
-						// And we don't want to materialize the zero register
-						// into a different register when it is just the
-						// argument to a store.
+					if vi.Rematerializeable && !costlyRemat(v) {
+						// Most rematerializations are a single instruction,
+						// and spilling another value to make room is not
+						// worth it. Nor do we want to materialize the zero
+						// register into a different register when it is just
+						// the argument to a store. Multi-instruction
+						// constants are worth building once outside the loop
+						// if a register is free (m below excludes registers
+						// holding likely-used values).
 						continue
 					}
 				}
diff --git a/src/cmd/compile/internal/ssacompile/regalloc_test.go b/src/cmd/compile/internal/ssacompile/regalloc_test.go
index ace4fb6..f31ba9b 100644
--- a/src/cmd/compile/internal/ssacompile/regalloc_test.go
+++ b/src/cmd/compile/internal/ssacompile/regalloc_test.go
@@ -410,6 +410,65 @@
 	}
 }
 
+// TestPreloadCostlyConstant checks that a constant needing several
+// instructions to build is loaded before a loop rather than rebuilt inside it,
+// while single-instruction constants are still rematerialized at their use.
+func TestPreloadCostlyConstant(t *testing.T) {
+	for _, tc := range []struct {
+		k      int64
+		inLoop bool
+	}{
+		{k: 7, inLoop: true},                    // MOVD
+		{k: -0x1234, inLoop: true},              // MOVN
+		{k: -0x30e44323485a9b9d, inLoop: false}, // MOVD + 3×MOVK (0xcf1bbcdcb7a56463)
+		{k: 0x12340000_5678, inLoop: false},     // MOVD + MOVK
+	} {
+		t.Run(fmt.Sprintf("%#x", uint64(tc.k)), func(t *testing.T) {
+			c := testConfigARM64(t)
+			i64 := c.config.Types.Int64
+			f := c.Fun("entry",
+				Bloc("entry",
+					Valu("mem", ssaop.OpInitMem, types.TypeMem, 0, nil),
+					Valu("addr", ssaop.OpArg, i64.PtrTo(), 0, c.Temp(i64.PtrTo())),
+					Valu("k", ssaop.OpARM64MOVDconst, i64, tc.k, nil),
+					Valu("init", ssaop.OpARM64MOVDconst, i64, 1, nil),
+					Goto("loopHead"),
+				),
+				Bloc("loopHead",
+					Valu("i", ssaop.OpPhi, i64, 0, nil, "init", "next"),
+					Valu("acc", ssaop.OpPhi, i64, 0, nil, "init", "prod"),
+					Valu("test", ssaop.OpARM64CMPconst, types.TypeFlags, 10, nil, "i"),
+					Eq("test", "exit", "loopBody"),
+				),
+				Bloc("loopBody",
+					Valu("next", ssaop.OpARM64ADDconst, i64, 1, nil, "i"),
+					Valu("prod", ssaop.OpARM64MUL, i64, 0, nil, "acc", "k"),
+					Goto("loopHead"),
+				),
+				Bloc("exit",
+					Valu("store", ssaop.OpARM64MOVDstore, types.TypeMem, 0, nil, "addr", "acc", "mem"),
+					Exit("store"),
+				),
+			)
+			f.f.Blocks[1].Likely = ssa.BranchUnlikely
+			regalloc(f.f)
+			checkFunc(f.f)
+
+			n := 0
+			for _, b := range []string{"loopHead", "loopBody"} {
+				for _, v := range f.blocks[b].Values {
+					if v.Op == ssaop.OpARM64MOVDconst && v.AuxInt == tc.k {
+						n++
+					}
+				}
+			}
+			if got := n > 0; got != tc.inLoop {
+				t.Errorf("constant materialized in loop = %v, want %v:\n%v", got, tc.inLoop, f.f)
+			}
+		})
+	}
+}
+
 // TestStartRegsDrop tests dropping physical register bits from startRegs preventing
 // unnecessary OpLoadReg on edges, such as edge right->merge in this example.
 //