Implement TARGET_CALLEE_SAVE_COST and TARGET_FRAME_ALLOCATION_COST so
IRA weighs callee saves against spills at their marginal execution
cost: reusing an already-required save is free, completing an odd
save count is discounted by half the memory cost (the fusion-aware
scheduling keeps the pair adjacent), and the first spill of a
frameless leaf is charged the separate SP adjustment.

This is a first, deliberately simple attempt, posted as a draft to
collect suggestions on the approach.  The model is unweighted and
knows nothing about emission; with more precise modeling (call and
spill frequency weighting, awareness of the final save layout) the
fusion characteristics of the target should translate into more
reliable gains.

Assisted-by: qwen3.8-max

gcc/ChangeLog:

        * config/riscv/riscv.cc (riscv_ira_save_mask): New.
        (riscv_frame_allocation_cost): New.
        (riscv_callee_save_cost): New.
        (TARGET_FRAME_ALLOCATION_COST, TARGET_CALLEE_SAVE_COST): Define.

gcc/testsuite/ChangeLog:

        * gcc.target/riscv/callee-save-cost-mempair.c: New test.
        * gcc.target/riscv/callee-save-cost-fusion.c: New test.
---
 gcc/config/riscv/riscv.cc                     | 96 +++++++++++++++++++
 .../riscv/callee-save-cost-fusion.c           | 21 ++++
 .../riscv/callee-save-cost-mempair.c          | 22 +++++
 3 files changed, 139 insertions(+)
 create mode 100644 gcc/testsuite/gcc.target/riscv/callee-save-cost-fusion.c
 create mode 100644 gcc/testsuite/gcc.target/riscv/callee-save-cost-mempair.c

diff --git a/gcc/config/riscv/riscv.cc b/gcc/config/riscv/riscv.cc
index 0deedcc4beb..72e5ac3a31a 100644
--- a/gcc/config/riscv/riscv.cc
+++ b/gcc/config/riscv/riscv.cc
@@ -11864,6 +11864,98 @@ riscv_memory_move_cost (machine_mode mode, reg_class_t 
rclass, bool in)
          + memory_move_secondary_cost (mode, rclass, in));
 }
 
+/* Return the save mask for registers FIRST..FIRST+31, combining IRA's
+   ALLOCATED set with the saves the function already requires.  */
+
+static unsigned int
+riscv_ira_save_mask (const HARD_REG_SET &allocated, unsigned int first)
+{
+  unsigned int mask = 0;
+  for (unsigned int i = 0; i < 32; ++i)
+    {
+      unsigned int regno = first + i;
+      /* Use known RA requirements instead of scanning RTL for jumps.  */
+      if (TEST_HARD_REG_BIT (allocated, regno)
+         || (regno == RETURN_ADDR_REGNUM
+             ? !crtl->is_leaf || df_regs_ever_live_p (regno)
+               || cfun->machine->far_jump_used
+             : riscv_save_reg_p (regno)))
+       mask |= 1U << i;
+    }
+  return mask;
+}
+
+/* Implement TARGET_FRAME_ALLOCATION_COST.  Charge the SP adjustment the
+   first spill of a frameless function needs; an existing frame absorbs
+   the spill.  FIXME: Account for push/pop and save-libcall frames.  */
+
+static int
+riscv_frame_allocation_cost (frame_cost_type, const HARD_REG_SET &allocated)
+{
+  /* An existing frame, a frame pointer, local variables, outgoing
+     arguments or any already-committed callee save absorbs the spill:
+     no separate SP adjustment is needed.  Naked functions have no
+     generated frame at all.  */
+  if (cfun->machine->naked_p || !crtl->is_leaf || frame_pointer_needed
+      || cfun->calls_alloca || crtl->calls_eh_return
+      || cfun->machine->varargs_size || crtl->args.pretend_args_size
+      || maybe_ne (get_frame_size () + crtl->outgoing_args_size, 0)
+      || !hard_reg_set_empty_p (allocated)
+      || riscv_ira_save_mask (allocated, GP_REG_FIRST)
+      || (TARGET_HARD_FLOAT && riscv_ira_save_mask (allocated, FP_REG_FIRST)))
+    return 0;
+  /* Model the entry/exit addi pair as one GPR move.  */
+  return riscv_register_move_cost (word_mode, GR_REGS, GR_REGS);
+}
+
+/* Implement TARGET_CALLEE_SAVE_COST.  Return the marginal cost of one
+   scalar save or restore.  */
+
+static int
+riscv_callee_save_cost (spill_cost_type type, unsigned int regno,
+                       machine_mode mode, unsigned int nregs, int mem_cost,
+                       const HARD_REG_SET &allocated, bool existing_spills_p)
+{
+  /* Charge a new frame only while IRA has not committed to spilling.  */
+  int frame_cost = (existing_spills_p ? 0 : riscv_frame_allocation_cost
+                  (type == spill_cost_type::SAVE ? frame_cost_type::ALLOCATION
+                                                : 
frame_cost_type::DEALLOCATION,
+                   allocated));
+  bool gp = GP_REG_P (regno);
+  /* Keep special frames and multi-register candidates on the fallback.
+     Shrink-wrapping only moves the prologue; the number and adjacency
+     of the emitted saves are unchanged.  */
+  if (cfun->machine->naked_p || cfun->machine->interrupt_handler_p
+      || crtl->calls_eh_return || crtl->abi->id () != RISCV_CC_BASE
+      || TARGET_SAVE_RESTORE
+      || (!gp && !FP_REG_P (regno)) || nregs != 1
+      || hard_regno_nregs (regno, mode) != 1)
+    return mem_cost + frame_cost;
+
+  unsigned int first = gp ? GP_REG_FIRST : FP_REG_FIRST;
+  unsigned int mask = riscv_ira_save_mask (allocated, first);
+  unsigned int bit = 1U << (regno - first);
+  /* An already-required save costs nothing: the save and restore run
+     anyway, so the value parked there displaces spill code for free.  */
+  if (mask & bit)
+    return 0;
+
+  /* An odd save count leaves a partner at a descending offset, and the
+     scheduler positions same-base consecutive-offset pairs adjacently,
+     so completing a pair halves its issue cost.  The two saves still
+     issue as two instructions, hence only half the memory cost is
+     discounted.  FIXME: Refine XTheadMemPair reach and overlapping
+     fusion estimates.  */
+  unsigned HOST_WIDE_INT fusion
+    = (optimize_function_for_size_p (cfun) ? 0 : riscv_get_fusible_ops ());
+  if ((popcount_hwi (mask) & 1)
+      && ((gp && TARGET_XTHEADMEMPAIR)
+         || (fusion & (gp ? RISCV_FUSE_LDST_PAIR_DEC
+                          : RISCV_FUSE_FLDFST_PAIR_DEC))))
+    mem_cost /= 2;
+  return mem_cost + frame_cost;
+}
+
 /* Return the number of instructions that can be issued per cycle.  */
 
 static int
@@ -17353,6 +17445,10 @@ riscv_memtag_tag_bitsize ()
 #undef TARGET_CONDITIONAL_REGISTER_USAGE
 #define TARGET_CONDITIONAL_REGISTER_USAGE riscv_conditional_register_usage
 
+#undef TARGET_FRAME_ALLOCATION_COST
+#define TARGET_FRAME_ALLOCATION_COST riscv_frame_allocation_cost
+#undef TARGET_CALLEE_SAVE_COST
+#define TARGET_CALLEE_SAVE_COST riscv_callee_save_cost
 #undef TARGET_CLASS_MAX_NREGS
 #define TARGET_CLASS_MAX_NREGS riscv_class_max_nregs
 
diff --git a/gcc/testsuite/gcc.target/riscv/callee-save-cost-fusion.c 
b/gcc/testsuite/gcc.target/riscv/callee-save-cost-fusion.c
new file mode 100644
index 00000000000..773dbff4a52
--- /dev/null
+++ b/gcc/testsuite/gcc.target/riscv/callee-save-cost-fusion.c
@@ -0,0 +1,21 @@
+/* { dg-do compile { target rv64 } } */
+/* { dg-skip-if "" { *-*-* } { "-O0" "-O1" "-O[sgz]" "-flto" } } */
+/* { dg-options "-O2 -march=rv64gc -mabi=lp64d -mtune=xt-c9501fdvt" } */
+
+/* Verify the pair-completion discount: with an odd required save count
+   (ra), completing the pair with s0 costs half the memory cost, so IRA
+   keeps x in a callee-saved register instead of spilling it around the
+   call.  */
+
+extern void call (void);
+volatile int sink;
+
+void
+reuse_pair (int x)
+{
+  call ();
+  sink = x;
+}
+
+/* { dg-final { scan-assembler-times {\tsd\tra,} 1 } } */
+/* { dg-final { scan-assembler-times {\tsd\ts0,} 1 } } */
diff --git a/gcc/testsuite/gcc.target/riscv/callee-save-cost-mempair.c 
b/gcc/testsuite/gcc.target/riscv/callee-save-cost-mempair.c
new file mode 100644
index 00000000000..57178938576
--- /dev/null
+++ b/gcc/testsuite/gcc.target/riscv/callee-save-cost-mempair.c
@@ -0,0 +1,22 @@
+/* { dg-do compile { target rv64 } } */
+/* { dg-skip-if "" { *-*-* } { "-O0" "-O1" "-O3" "-O[sgz]" "-flto" } } */
+/* { dg-options "-O2 -march=rv64imac_xtheadmempair -mabi=lp64 -mtune=rocket" } 
*/
+/* { dg-additional-options "-fomit-frame-pointer -fno-shrink-wrap" } */
+
+/* Verify the pair-completion discount with XTheadMemPair: with an odd
+   required save count (ra), completing the pair with s0 is cheap, so
+   IRA saves both in one th.sdd instead of spilling x around the
+   call.  */
+
+extern void call (void);
+volatile float sink;
+
+void
+reuse_pair (float x)
+{
+  call ();
+  sink = x;
+}
+
+/* { dg-final { scan-assembler-times {th.sdd\s+} 1 } } */
+/* { dg-final { scan-assembler-times {th.ldd\s+} 1 } } */
-- 
2.52.0

Reply via email to