On Fri, Sep 25, 2026 at 2:37 PM Dominic P <[email protected]> wrote:
>
> On a STRICT_ALIGNMENT target store merging never widens a group below its
> alignment, even where movmisalign<mode> is one unaligned str or strh. Let
> such
> targets use the aligned/unaligned store-count comparison for each size whose
> movmisalign expansion, like every smaller one split_group may shrink it to,
> costs no more for size than the aligned accesses it replaces. arm's unaligned
> strh and str pass. Alpha without BWX (EV5) merges and shrinks; alpha with
> BWX,
> whose byte stores are cheaper, is unchanged.
>
> Pi1MHz firmware (arm1176, -O2, LTO): .text -416 bytes; a merged strh takes 30%
> fewer cycles than the two strb it replaces.
>
> Bootstrapped and regression tested on x86_64-pc-linux-gnu. Regression tested
> on arm-none-eabi (arm.exp, dg.exp, tree-ssa.exp; compile-only Cortex-A15 Arm
> and
> Thumb, Cortex-M0, Armv8-A); execute.exp, dg-torture.exp and dg.exp are
> unchanged
> on qemu arm1176 with the MMU on.
>
> Assisted-by: Claude Opus 5 (Anthropic)
>
> gcc/ChangeLog:
>
> * gimple-ssa-store-merging.cc: Include memmodel.h, emit-rtl.h and
> optabs.h.
> (max_access_size): New function.
> (imm_store_chain_info::try_coalesce_bswap): Use it for the store and
> the load alignment checks.
> (split_group): Use it to limit unaligned stores on STRICT_ALIGNMENT
> targets and loads below their alignment.
> (imm_store_chain_info::output_merged_store): Allow unaligned stores
> on STRICT_ALIGNMENT targets but not unaligned loads. Adjust the
> function comment.
>
> gcc/testsuite/ChangeLog:
>
> * gcc.dg/torture/store-merging-misaligned.c: New test.
> * gcc.target/arm/store-merge-unaligned.c: New test.
> * gcc.target/arm/store-merge-unaligned-2.c: New test.
> * gcc.target/arm/store-merge-unaligned-3.c: New test.
> * gcc.target/arm/store-merge-unaligned-4.c: New test.
>
> Signed-off-by: Dominic P <[email protected]>
> ---
> Changes in v2: no new hook (Andrea Pinski). Store merging asks about its own
> transform (Richard Biener): below the alignment it uses movmisalign<mode> only
> where that costs no more than the aligned accesses it replaces. The arm patch
> is gone; the tests check function bodies, with SLP off where it merged first.
>
> Building RTL at GIMPLE time for costs has precedent (ivopts' computation_cost,
> tree-ssa-address, reassoc and switch conversion); here it asks the target only
> about the one sequence the pass would emit.
>
> OK for trunk?
>
> gcc/gimple-ssa-store-merging.cc | 56 +++++++++--
> .../gcc.dg/torture/store-merging-misaligned.c | 43 ++++++++
> .../gcc.target/arm/store-merge-unaligned-2.c | 22 +++++
> .../gcc.target/arm/store-merge-unaligned-3.c | 20 ++++
> .../gcc.target/arm/store-merge-unaligned-4.c | 20 ++++
> .../gcc.target/arm/store-merge-unaligned.c | 99 +++++++++++++++++++
> 6 files changed, 253 insertions(+), 7 deletions(-)
> create mode 100644 gcc/testsuite/gcc.dg/torture/store-merging-misaligned.c
> create mode 100644 gcc/testsuite/gcc.target/arm/store-merge-unaligned-2.c
> create mode 100644 gcc/testsuite/gcc.target/arm/store-merge-unaligned-3.c
> create mode 100644 gcc/testsuite/gcc.target/arm/store-merge-unaligned-4.c
> create mode 100644 gcc/testsuite/gcc.target/arm/store-merge-unaligned.c
>
> diff --git a/gcc/gimple-ssa-store-merging.cc b/gcc/gimple-ssa-store-merging.cc
> index 39ef59ab9a6..7f086f1fb35 100644
> --- a/gcc/gimple-ssa-store-merging.cc
> +++ b/gcc/gimple-ssa-store-merging.cc
> @@ -166,7 +166,10 @@
> #include "target.h"
> #include "gimplify-me.h"
> #include "rtl.h"
> +#include "memmodel.h"
> +#include "emit-rtl.h"
> #include "expr.h" /* For get_bit_range. */
> +#include "optabs.h"
> #include "optabs-tree.h"
> #include "dbgcnt.h"
> #include "selftest.h"
> @@ -2952,6 +2955,44 @@ check_no_overlap (const vec<store_immediate_info *>
> &m_store_info,
> return true;
> }
>
> +/* Return the largest power of two of at most SIZE bits that a load (if LOAD)
> + or store at ALIGN bits of alignment may use. A size above ALIGN needs a
> + movmisalign whose size cost, like that of every smaller such size, is no
> + more than that of the aligned accesses it replaces. */
> +
> +static unsigned HOST_WIDE_INT
> +max_access_size (unsigned HOST_WIDE_INT size, unsigned HOST_WIDE_INT align,
> + bool load)
> +{
> + unsigned HOST_WIDE_INT s = MIN (size, align);
> + if (!param_store_merging_allow_unaligned || s == size)
> + return s;
> + rtx addr = gen_raw_REG (Pmode, LAST_VIRTUAL_REGISTER + 1);
> + rtx mem = gen_rtx_MEM (int_mode_for_size (s, 0).require (), addr);
> + rtx reg = gen_raw_REG (GET_MODE (mem), LAST_VIRTUAL_REGISTER + 2);
> + start_sequence ();
> + emit_move_insn (load ? reg : mem, load ? mem : reg);
> + unsigned aligned_cost = seq_cost (end_sequence (), false);
> + for (; s < size; s *= 2)
> + {
> + scalar_int_mode mode = int_mode_for_size (s * 2, 0).require ();
it seems 'size' needs to be power-of-two but this isn't documented?
You should probably be using FOR_EACH_WIDER_MODE
or FOR_EACH_2XWIDER_MODE which is more efficient.
> + insn_code icode = optab_handler (movmisalign_optab, mode);
> + if (icode == CODE_FOR_nothing)
Not sure if we have such target, but targetm.slow_unaligned_access
might be false for 'mode'. We do have targets that do not define
STRICT_ALIGNMENT but have targetm.slow_unaligned_access for
some modes.
As indicated in the previous mail we should eliminate the STRICT_ALIGNMENT
macro in favor of testing targetm.slow_unaligned_access.
> + break;
> + mem = gen_rtx_MEM (mode, addr);
> + set_mem_align (mem, align);
> + reg = gen_raw_REG (mode, LAST_VIRTUAL_REGISTER + 2);
> + class expand_operand ops[2];
> + create_fixed_operand (&ops[0], load ? reg : mem);
> + create_fixed_operand (&ops[1], load ? mem : reg);
> + start_sequence ();
> + expand_insn (icode, 2, ops);
> + if (seq_cost (end_sequence (), false) > aligned_cost * (s * 2 / align))
> + break;
Given all this incurs quite some cost the result should be cached somehow,
possibly allowing a cheap 'is size ok for align' query as well rather than only
finding the maximum.
> + }
> + return s;
> +}
> +
> /* Return true if m_store_info[first] and at least one following store
> form a group which store try_size bitsize value which is byte swapped
> from a memory load or some value, or identity from some value.
> @@ -3006,7 +3047,7 @@ imm_store_chain_info::try_coalesce_bswap
> (merged_store_group *merged_store,
> = (m_store_info[first]->bitpos - align_base) & (align - 1);
> if (align_bitpos)
> align = least_bit_hwi (align_bitpos);
> - if (align < try_size)
> + if (max_access_size (try_size, align, false) < try_size)
this needs the answer for exactly one size, not the "max"?
> return false;
> }
>
> @@ -3145,7 +3186,7 @@ imm_store_chain_info::try_coalesce_bswap
> (merged_store_group *merged_store,
> if (!allow_unaligned && n.base_addr)
> {
> unsigned int align = get_object_alignment (n.src);
> - if (align < try_size)
> + if (max_access_size (try_size, align, true) < try_size)
> return false;
> }
>
> @@ -3933,6 +3974,8 @@ split_group (merged_store_group *group, bool
> allow_unaligned_store,
> align = least_bit_hwi (align_bitpos);
> if (!allow_unaligned_store)
> try_size = MIN (try_size, align);
> + else if (STRICT_ALIGNMENT)
> + try_size = max_access_size (try_size, align, false);
> if (!allow_unaligned_load)
> {
> /* If we can't do or don't want to do unaligned stores
> @@ -3956,7 +3999,7 @@ split_group (merged_store_group *group, bool
> allow_unaligned_store,
> load_align = MIN (load_align, a);
> }
> }
> - try_size = MIN (try_size, load_align);
> + try_size = max_access_size (try_size, load_align, true);
You said you are not adjusting loads?
> }
> store_immediate_info *info
> = find_constituent_stores (group, NULL, &first, try_bitpos, try_size);
> @@ -4229,7 +4272,7 @@ invert_op (split_store *split_store, int idx, tree
> int_type, tree &mask)
> /* Given a merged store group GROUP output the widened version of it.
> The store chain is against the base object BASE.
> Try store sizes of at most MAX_STORE_BITSIZE bits wide and don't output
> - unaligned stores for STRICT_ALIGNMENT targets or if it's too expensive.
> + unaligned stores if it's too expensive.
> Make sure that the number of statements output is less than the number of
> original statements. If a better sequence is possible emit it and
> return true. */
> @@ -4243,9 +4286,8 @@ imm_store_chain_info::output_merged_store
> (merged_store_group *group)
> if (orig_num_stmts < 2)
> return false;
>
> - bool allow_unaligned_store
> - = !STRICT_ALIGNMENT && param_store_merging_allow_unaligned;
> - bool allow_unaligned_load = allow_unaligned_store;
> + bool allow_unaligned_store = param_store_merging_allow_unaligned;
> + bool allow_unaligned_load = !STRICT_ALIGNMENT && allow_unaligned_store;
> bool bzero_first = false;
> store_immediate_info *store;
> unsigned int num_clobber_stmts = 0;
> diff --git a/gcc/testsuite/gcc.dg/torture/store-merging-misaligned.c
> b/gcc/testsuite/gcc.dg/torture/store-merging-misaligned.c
> new file mode 100644
> index 00000000000..6a8e8791fee
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/torture/store-merging-misaligned.c
> @@ -0,0 +1,43 @@
> +/* Byte stores merged below their alignment must store the right bytes. */
> +
> +/* { dg-do run } */
> +
> +typedef __UINT8_TYPE__ u8;
> +typedef __UINT32_TYPE__ u32;
> +
> +static u8 buf[24];
> +
> +static void __attribute__((noipa))
> +st_h (u8 *p, u32 v) { p[0] = v; p[1] = v >> 8; }
> +
> +static void __attribute__((noipa))
> +st_w (u8 *p, u32 v)
> +{ p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
> +
> +static void __attribute__((noipa))
> +st_wbe (u8 *p, u32 v)
> +{ p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; }
> +
> +int
> +main (void)
> +{
> + for (int off = 0; off < 8; off++)
> + {
> + __builtin_memset (buf, 0xa5, sizeof (buf));
> + st_h (buf + off, 0x1234);
> + st_w (buf + off + 4, 0x89abcdef);
> + st_wbe (buf + off + 12, 0x89abcdef);
> + if (buf[off] != 0x34 || buf[off + 1] != 0x12)
> + __builtin_abort ();
> + if (buf[off + 4] != 0xef || buf[off + 5] != 0xcd
> + || buf[off + 6] != 0xab || buf[off + 7] != 0x89)
> + __builtin_abort ();
> + if (buf[off + 12] != 0x89 || buf[off + 13] != 0xab
> + || buf[off + 14] != 0xcd || buf[off + 15] != 0xef)
> + __builtin_abort ();
> + if (buf[off + 2] != 0xa5 || buf[off + 3] != 0xa5
> + || buf[off + 8] != 0xa5 || buf[off + 11] != 0xa5)
> + __builtin_abort ();
> + }
> + return 0;
> +}
> diff --git a/gcc/testsuite/gcc.target/arm/store-merge-unaligned-2.c
> b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-2.c
> new file mode 100644
> index 00000000000..3517c9c3c41
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-2.c
> @@ -0,0 +1,22 @@
> +/* Without unaligned access there is no movmisalign, so nothing merges. */
> +
> +/* { dg-do compile } */
> +/* { dg-require-effective-target arm_arch_v6_arm_ok } */
> +/* { dg-options "-O2 -mno-unaligned-access -fno-tree-slp-vectorize" } */
> +/* { dg-add-options arm_arch_v6_arm } */
> +
> +typedef unsigned char u8;
> +typedef unsigned int u32;
> +
> +void h_le (u8 *p, u32 v) { p[0] = v; p[1] = v >> 8; }
> +void w_le (u8 *p, u32 v)
> +{ p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
> +void cph (u8 *__restrict d, const u8 *__restrict q)
> +{
> + u8 *p = __builtin_assume_aligned (d, 2);
> + p[0] = q[0]; p[1] = q[1];
> +}
> +
> +/* { dg-final { scan-assembler-not {\tstrh\t} } } */
> +/* { dg-final { scan-assembler-not {\tstr\t} } } */
> +/* { dg-final { scan-assembler-not {\tldrh\t} } } */
> diff --git a/gcc/testsuite/gcc.target/arm/store-merge-unaligned-3.c
> b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-3.c
> new file mode 100644
> index 00000000000..bd2e2c26ea9
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-3.c
> @@ -0,0 +1,20 @@
> +/* --param=store-merging-allow-unaligned=0 stops the unaligned merges. */
> +
> +/* { dg-do compile } */
> +/* { dg-require-effective-target arm_arch_v6_arm_ok } */
> +/* { dg-options "-O2 -fno-tree-slp-vectorize
> --param=store-merging-allow-unaligned=0" } */
> +/* { dg-add-options arm_arch_v6_arm } */
> +
> +typedef unsigned char u8;
> +typedef unsigned int u32;
> +
> +void h_le (u8 *p, u32 v) { p[0] = v; p[1] = v >> 8; }
> +void h_be (u8 *p, u32 v) { p[0] = v >> 8; p[1] = v; }
> +void cph (u8 *__restrict d, const u8 *__restrict q)
> +{
> + u8 *p = __builtin_assume_aligned (d, 2);
> + p[0] = q[0]; p[1] = q[1];
> +}
> +
> +/* { dg-final { scan-assembler-not {\tstrh\t} } } */
> +/* { dg-final { scan-assembler-not {\tldrh\t} } } */
> diff --git a/gcc/testsuite/gcc.target/arm/store-merge-unaligned-4.c
> b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-4.c
> new file mode 100644
> index 00000000000..9cdf9f7bf14
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/arm/store-merge-unaligned-4.c
> @@ -0,0 +1,20 @@
> +/* The bswap path: a reversed unaligned copy becomes ldr, rev and str. */
> +
> +/* { dg-do compile } */
> +/* { dg-require-effective-target arm_arch_v6_arm_ok } */
> +/* { dg-options "-O2 -fno-tree-slp-vectorize" } */
> +/* { dg-add-options arm_arch_v6_arm } */
> +/* { dg-add-options check_function_bodies } */
> +/* { dg-final { check-function-bodies "**" "" } } */
> +
> +typedef unsigned char u8;
> +
> +/*
> +** copy_be:
> +** ldr (r[0-9]+), \[r1\] @ unaligned
> +** rev \1, \1
> +** str \1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void copy_be (u8 *__restrict p, const u8 *__restrict q)
> +{ p[0] = q[3]; p[1] = q[2]; p[2] = q[1]; p[3] = q[0]; }
> diff --git a/gcc/testsuite/gcc.target/arm/store-merge-unaligned.c
> b/gcc/testsuite/gcc.target/arm/store-merge-unaligned.c
> new file mode 100644
> index 00000000000..c3dd7350dde
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/arm/store-merge-unaligned.c
> @@ -0,0 +1,99 @@
> +/* Stores below their alignment merge into unaligned strh or str on Armv6.
> */
> +
> +/* { dg-do compile } */
> +/* { dg-require-effective-target arm_arch_v6_arm_ok } */
> +/* { dg-require-effective-target arm_little_endian } */
> +/* { dg-options "-O2 -fno-tree-slp-vectorize" } */
> +/* { dg-add-options arm_arch_v6_arm } */
> +/* { dg-add-options check_function_bodies } */
> +/* { dg-final { check-function-bodies "**" "" } } */
> +
> +typedef unsigned char u8;
> +typedef unsigned short u16;
> +typedef unsigned int u32;
> +
> +/*
> +** h_le:
> +** strh r1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void h_le (u8 *p, u32 v) { p[0] = v; p[1] = v >> 8; }
> +
> +/*
> +** h_be:
> +** rev16 (r[0-9]+), r1
> +** strh \1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void h_be (u8 *p, u32 v) { p[0] = v >> 8; p[1] = v; }
> +
> +/*
> +** w_le:
> +** str r1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void w_le (u8 *p, u32 v)
> +{ p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
> +
> +/*
> +** w_be:
> +** rev (r[0-9]+), r1
> +** str \1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void w_be (u8 *p, u32 v)
> +{ p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; }
> +
> +/*
> +** hh:
> +** str r1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void hh (u16 *p, u32 v) { p[0] = v; p[1] = v >> 16; }
> +
> +/*
> +** t3:
> +** lsr (r[0-9]+), r1, #16
> +** strh r1, \[r0\] @ unaligned
> +** strb \1, \[r0, #2\]
> +** bx lr
> +*/
> +void t3 (u8 *p, u32 v) { p[0] = v; p[1] = v >> 8; p[2] = v >> 16; }
> +
> +/*
> +** cp4:
> +** ldr (r[0-9]+), \[r1\] @ unaligned
> +** str \1, \[r0\] @ unaligned
> +** bx lr
> +*/
> +void cp4 (u8 *__restrict p, const u8 *__restrict q)
> +{ p[0] = q[0]; p[1] = q[1]; p[2] = q[2]; p[3] = q[3]; }
> +
> +/*
> +** cpa:
> +** ldr (r[0-9]+), \[r1\] @ unaligned
> +** str \1, \[r0\]
> +** bx lr
> +*/
> +void cpa (u8 *__restrict d, const u8 *__restrict q)
> +{
> + u8 *p = __builtin_assume_aligned (d, 4);
> + p[0] = q[0]; p[1] = q[1]; p[2] = q[2]; p[3] = q[3];
> +}
> +
> +struct ctx { u32 s[2]; };
> +
> +static void st (u8 *p, u32 v)
> +{ p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
> +
> +/* C and OUT may alias, so only store merging can widen these. */
> +
> +/*
> +** w:
> +** ldr (r[0-9]+), \[r0\]
> +** str \1, \[r1\] @ unaligned
> +** ldr (r[0-9]+), \[r0, #4\]
> +** str \2, \[r1, #4\] @ unaligned
> +** bx lr
> +*/
> +void w (struct ctx *c, u8 *out) { st (out, c->s[0]); st (out + 4, c->s[1]); }
> --
> 2.55.0
>