From: Kyrylo Tkachov <[email protected]>
For a loop such as
void copyReverseGeneric (int *dst, int *src)
{
for (int i = 0; i < 10000; ++i)
dst[i] = __builtin_bswap32 (src[i]);
}
vectorised and unrolled by two, ivopts can address the memory references
either with a single shared "index" IV and register-offset addressing
(reg + reg), or with one pointer IV per stream and immediate-offset
addressing (reg + imm). It prices the two forms equally, so it falls
back on its existing preference for fewer IVs and picks the shared index.
On targets without a "base + index + offset" addressing mode that is
unfortunate. Since the constant offset cannot be folded in, the shared
index needs a separate base register for every consecutive access (e.g.
src and src + 16), whereas a single per-base pointer IV reaches all of a
stream's accesses through immediate offsets. So even without pairing the
register-offset form wastes registers held live across the loop, and
where the target has load/store-pair instructions it additionally
prevents the accesses from being combined into a pair. The
immediate-offset form avoids both.
The distinguishing signal is whether the target can fold a constant
displacement into a register-offset address. A new helper,
addr_reg_offset_imm_valid_p, probes this with valid_mem_ref_p. When the
target cannot, and the use has a pairing sibling (from the preceding
patch), get_address_cost charges one extra unit to the register-offset
form when optimising for speed, so selection prefers the immediate-offset
form once enough accesses would pair to pay for the extra IV.
AFAICT ivopts already preferences fewer candidates, chiefly through the
"return cost + n_cands" term in ivopts_estimate_reg_pressure.
The cost added here stays out of that
machinery: it is not a function of the candidate count, but is charged
per address use and only to the register-offset form, entering the set
cost solely through cand_use_cost, never through cand_cost or the n_cands
term. It therefore adds no per-candidate bias and does not make larger
candidate sets unattractive; it only raises the price of register-offset
addressing for pairable accesses. Like every address cost it is scaled
by block frequency (one unit per use in the common single-loop case), and
accumulated over a group it is of the same order as the one extra IV the
immediate-offset form needs, so an isolated access is left alone.
The heuristic is implemented in target-independent code and adds no new
target hooks; it uses the existing valid_mem_ref_p query to test whether
the target can fold a constant offset into a register-offset address. It
is motivated by aarch64, which cannot fold the offset and which has
LDP/STP. On targets that can fold it, such as x86, the change is a
no-op: the penalty fires zero times and a bootstrap there is bit-identical
to an unpatched one. For the example above the loop now compiles to a
post-increment LDP/STP on aarch64:
.L3:
ldp q31, q30, [x2], 32
rev32 v31.16b, v31.16b
rev32 v30.16b, v30.16b
stp q31, q30, [x3], 32
cmp x2, x0
bne .L3
Bootstrapped and regression-tested on aarch64-unknown-linux-gnu with no
regressions; also bootstrapped on x86_64-pc-linux-gnu, where it is a
no-op. This triggers ~5k times in SPEC2026 where it produces more
LDP/STPs and reduces the overall text size.
Ok for trunk?
Thanks,
Kyrill
gcc/ChangeLog:
PR target/121315
* tree-ssa-loop-ivopts.cc (addr_reg_offset_imm_valid_p): New function.
(get_address_cost): Charge a small extra cost for register-offset
addressing of a use that has a pairing sibling when the target cannot
fold a constant offset into the address.
gcc/testsuite/ChangeLog:
PR target/121315
* gcc.target/aarch64/pr121315.c: New test.
* gcc.target/aarch64/pr121315-2.c: New test.
Signed-off-by: Kyrylo Tkachov <[email protected]>
---
gcc/testsuite/gcc.target/aarch64/pr121315-2.c | 31 +++++++++++
gcc/testsuite/gcc.target/aarch64/pr121315.c | 15 ++++++
gcc/tree-ssa-loop-ivopts.cc | 51 +++++++++++++++++++
3 files changed, 97 insertions(+)
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr121315-2.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr121315.c
diff --git a/gcc/testsuite/gcc.target/aarch64/pr121315-2.c
b/gcc/testsuite/gcc.target/aarch64/pr121315-2.c
new file mode 100644
index 00000000000..c76b4e0480c
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr121315-2.c
@@ -0,0 +1,31 @@
+/* PR target/121315: consecutive accesses that share a base should be
+ addressed with an immediate offset (a per-base pointer IV) rather than a
+ register offset, so the load/store-pair pass can fuse them. Check this
+ holds for several element types. */
+/* { dg-do compile } */
+/* { dg-options "-O3 -mcpu=neoverse-v2" } */
+
+void
+copy_add_l (long *d, long *s)
+{
+ for (int i = 0; i < 1000; ++i)
+ d[i] = s[i] + 1;
+}
+
+void
+copy_mul_f (float *d, float *s)
+{
+ for (int i = 0; i < 1000; ++i)
+ d[i] = s[i] * 2.0f;
+}
+
+void
+copy_add_d (double *d, double *s)
+{
+ for (int i = 0; i < 1000; ++i)
+ d[i] = s[i] + s[i];
+}
+
+/* Each loop should use an LDP and an STP of q-registers. */
+/* { dg-final { scan-assembler-times {ldp\tq[0-9]+, q[0-9]+} 3 } } */
+/* { dg-final { scan-assembler-times {stp\tq[0-9]+, q[0-9]+} 3 } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/pr121315.c
b/gcc/testsuite/gcc.target/aarch64/pr121315.c
new file mode 100644
index 00000000000..fa71f9e0acc
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr121315.c
@@ -0,0 +1,15 @@
+/* PR target/121315: ivopts should prefer immediate-offset addressing for
+ consecutive accesses that share a base, so the accesses can be combined
+ into LDP/STP rather than using register-offset addressing that blocks it.
*/
+/* { dg-do compile } */
+/* { dg-options "-O3 -mcpu=neoverse-v2" } */
+
+void
+copyReverseGeneric (int *dst, int *src)
+{
+ for (int i = 0; i < 10000; ++i)
+ dst[i] = __builtin_bswap32 (src[i]);
+}
+
+/* { dg-final { scan-assembler {ldp\tq[0-9]+, q[0-9]+} } } */
+/* { dg-final { scan-assembler {stp\tq[0-9]+, q[0-9]+} } } */
diff --git a/gcc/tree-ssa-loop-ivopts.cc b/gcc/tree-ssa-loop-ivopts.cc
index 557b35ed2a1..dc86cedaf12 100644
--- a/gcc/tree-ssa-loop-ivopts.cc
+++ b/gcc/tree-ssa-loop-ivopts.cc
@@ -4699,6 +4699,34 @@ get_address_cost_ainc (poly_int64 ainc_step, poly_int64
ainc_offset,
return infinite_cost;
}
+/* Return true if the target supports a register-offset address that also
+ has a constant displacement, i.e. "base + index + offset", for memory of
+ MEM_MODE in address space AS. On such targets a single register-offset
+ induction variable can serve several accesses that share a base but differ
+ by a constant offset, by folding the offset into each address. When it is
+ false, each such access needs its own base register if register-offset
+ addressed, so immediate-offset addressing (a per-base pointer IV) is
+ preferable. */
+
+static bool
+addr_reg_offset_imm_valid_p (machine_mode mem_mode, addr_space_t as,
+ code_helper code = ERROR_MARK)
+{
+ HOST_WIDE_INT size;
+ /* Use the access size as a representative consecutive offset; bail out for
+ modes without a compile-time constant size, treating them as if the
+ offset were foldable so that no penalty is applied. This is a
+ deliberately conservative probe (unscaled index, a single representative
+ displacement): it is exact for targets that have no register-offset plus
+ immediate form at all, and on other targets it only affects costs. */
+ if (!GET_MODE_SIZE (mem_mode).is_constant (&size) || size == 0)
+ return true;
+
+ struct mem_address parts = {NULL_TREE, integer_one_node, integer_one_node,
+ NULL_TREE, build_int_cst (sizetype, size)};
+ return valid_mem_ref_p (mem_mode, as, &parts, code);
+}
+
/* Return cost of computing USE's address expression by using CAND.
AFF_INV and AFF_VAR represent invariant and variant parts of the
address expression, respectively. If AFF_INV is simple, store
@@ -4859,6 +4887,29 @@ get_address_cost (struct ivopts_data *data, struct
iv_use *use,
if (parts.offset != NULL_TREE && !integer_zerop (parts.offset))
cost.complexity += 1;
+ /* This is a register-offset address form: a loop-invariant base register
+ (COMP_INV, possibly with a symbol folded in) plus the candidate used as an
+ index register. If the use shares its base with another access in the
+ loop, and the target cannot fold a constant displacement into such an
+ address, then each access needs a distinct base register and the accesses
+ cannot be combined into a load/store pair. Charge one address-cost unit
+ per such use: accumulated over the group's uses this offsets roughly the
+ cost of the extra induction variable that the immediate-offset form needs,
+ so per-base pointer IVs are preferred once enough accesses would pair to
+ make that worthwhile, while a single isolated access is left alone. */
+ if (speed
+ && comp_inv != NULL_TREE
+ && parts.index != NULL_TREE
+ && use->has_pairing_sibling
+ && !addr_reg_offset_imm_valid_p (mem_mode, as, code))
+ {
+ cost.cost += 1;
+ if (dump_file && (dump_flags & TDF_DETAILS))
+ fprintf (dump_file, "Penalizing register-offset address for use %d.%d:"
+ " base is shared with a pairable sibling access.\n",
+ use->group_id, use->id);
+ }
+
return cost;
}
--
2.50.1 (Apple Git-155)