On Mon, Jul 6, 2026 at 9:37 AM Andrew Gaul <[email protected]> wrote: > > Running x86lint, a linter for suboptimal x86-64 instruction encodings, over > GCC's own -O2 binaries flags every 128-bit integer load as one byte larger > than necessary. > > TARGET_SSE_TYPELESS_STORES already emits the one-byte-shorter movaps/movups > for 128-bit SSE stores rather than movdqa/movdqu, which differ only by a 66/F3 > prefix (the saving does not exist under AVX/AVX512, so it is disabled there). > The identical size win is available for 128-bit loads but has never been > taken: > an integer 128-bit load uses movdqa/movdqu for every -mtune. > > Add the symmetric X86_TUNE_SSE_TYPELESS_LOADS, enabled for the same > processors, > and use it in the 128-bit move patterns so a load emits movaps/movups > (aligned/unaligned respectively). The substitution is value- and > alignment-exact -- movaps/movdqa both fault on 16-byte-unaligned memory, > movups/movdqu neither do -- and the existing AVX guard in the move patterns > leaves VEX-encoded loads unchanged. It shrinks every 128-bit struct/array > copy > load by a byte, matching the code clang already generates. > > The historical asymmetry appears to be an omission rather than a deliberate > choice: typeless stores was introduced as a pure size optimization and no > typeless-loads counterpart was ever proposed; the load mnemonic has otherwise > only been revisited for correctness (e.g. PR target/92904). On the targeted > cores a 128-bit load feeding an integer-domain consumer has no bypass > ("reformatting") penalty, so as with the store the choice is size-only.
No, the asymmetry is not an omission. The existing TARGET_SSE_TYPELESS_STORES rationale works because a store is a one-way trip — data leaves the register file, so whatever "domain" (int vs. float) the store instruction pretends the data is in doesn't matter to anything downstream. There's no consumer to suffer a bypass delay. A load is different: it populates a register that something else is going to read. If TARGET_SSE_TYPELESS_LOADS fires unconditionally in mov<mode>_internal/*movti_internal regardless of what consumes the loaded value, then a case like v4si a = *p; v4si b = a + c; // padd, integer domain now loads with movaps and immediately feeds an integer-domain SSE op. On the classic P6-lineage/Atom-style domain-bypass model, that's exactly the pattern the movdqa/movaps distinction was invented to avoid — a real (not just cosmetic) forwarding penalty, not merely an encoding-size question. Stores can be typeless for free, loads can't in general because the consumer is unknown at the point this pattern fires. > > gcc/ChangeLog: > > * config/i386/x86-tune.def (X86_TUNE_SSE_TYPELESS_LOADS): New tune > flag. > * config/i386/i386.h (TARGET_SSE_TYPELESS_LOADS): New macro. > * config/i386/sse.md (mov<mode>_internal): Select V4SF mode for a > 128-bit load when TARGET_SSE_TYPELESS_LOADS, so ix86_output_ssemov > emits the shorter movups/movaps. > * config/i386/i386.md (*movti_internal): Likewise. > > gcc/testsuite/ChangeLog: > > * gcc.target/i386/sse-typeless-loads.c: New test. > > Signed-off-by: Andrew Gaul <[email protected]> > --- > gcc/config/i386/i386.h | 2 ++ > gcc/config/i386/i386.md | 4 +++ > gcc/config/i386/sse.md | 4 +++ > gcc/config/i386/x86-tune.def | 7 +++++ > .../gcc.target/i386/sse-typeless-loads.c | 28 +++++++++++++++++++ > 5 files changed, 45 insertions(+) > create mode 100644 gcc/testsuite/gcc.target/i386/sse-typeless-loads.c > > diff --git a/gcc/config/i386/i386.h b/gcc/config/i386/i386.h > index 1b8785d7872..d0717e88794 100644 > --- a/gcc/config/i386/i386.h > +++ b/gcc/config/i386/i386.h > @@ -394,6 +394,8 @@ extern unsigned char ix86_tune_features[X86_TUNE_LAST]; > #define TARGET_SSE_SPLIT_REGS ix86_tune_features[X86_TUNE_SSE_SPLIT_REGS] > #define TARGET_SSE_TYPELESS_STORES \ > ix86_tune_features[X86_TUNE_SSE_TYPELESS_STORES] > +#define TARGET_SSE_TYPELESS_LOADS \ > + ix86_tune_features[X86_TUNE_SSE_TYPELESS_LOADS] > #define TARGET_SSE_LOAD0_BY_PXOR > ix86_tune_features[X86_TUNE_SSE_LOAD0_BY_PXOR] > #define TARGET_MEMORY_MISMATCH_STALL \ > ix86_tune_features[X86_TUNE_MEMORY_MISMATCH_STALL] > diff --git a/gcc/config/i386/i386.md b/gcc/config/i386/i386.md > index f173423b334..524576b180b 100644 > --- a/gcc/config/i386/i386.md > +++ b/gcc/config/i386/i386.md > @@ -2577,6 +2577,10 @@ > (and (eq_attr "alternative" "5") > (match_test "TARGET_SSE_TYPELESS_STORES")) > (const_string "V4SF") > + (and (eq_attr "alternative" "4") > + (match_test "MEM_P (operands[1])") > + (match_test "TARGET_SSE_TYPELESS_LOADS")) > + (const_string "V4SF") > ] > (const_string "TI"))) > (set (attr "preferred_for_speed") > diff --git a/gcc/config/i386/sse.md b/gcc/config/i386/sse.md > index 7f652a13c87..7b2e785713a 100644 > --- a/gcc/config/i386/sse.md > +++ b/gcc/config/i386/sse.md > @@ -1521,6 +1521,10 @@ > (and (eq_attr "alternative" "4") > (match_test "TARGET_SSE_TYPELESS_STORES")) > (const_string "V4SF") > + (and (eq_attr "alternative" "3") > + (match_test "MEM_P (operands[1])") > + (match_test "TARGET_SSE_TYPELESS_LOADS")) > + (const_string "V4SF") > (and (eq_attr "alternative" "0") > (match_test "TARGET_SSE_LOAD0_BY_PXOR")) > (const_string "TI") > diff --git a/gcc/config/i386/x86-tune.def b/gcc/config/i386/x86-tune.def > index d4c27351ad7..fbc5b352ed5 100644 > --- a/gcc/config/i386/x86-tune.def > +++ b/gcc/config/i386/x86-tune.def > @@ -472,6 +472,13 @@ DEF_TUNE (X86_TUNE_SSE_TYPELESS_STORES, > "sse_typeless_stores", > m_AMD_MULTIPLE | m_ZHAOXIN | m_CORE_ALL | m_TREMONT | m_CORE_HYBRID > | m_CORE_ATOM | m_C86_4G | m_GENERIC) > > +/* X86_TUNE_SSE_TYPELESS_LOADS: Always movaps/movups for 128bit loads. As > with > + typeless stores, the shorter (no 66/F3 prefix) encoding is used; on these > + cores a load feeding an integer-domain consumer has no bypass penalty. */ > +DEF_TUNE (X86_TUNE_SSE_TYPELESS_LOADS, "sse_typeless_loads", > + m_AMD_MULTIPLE | m_ZHAOXIN | m_CORE_ALL | m_TREMONT | m_CORE_HYBRID > + | m_CORE_ATOM | m_C86_4G | m_GENERIC) Is this list actually derived from bypass-penalty data, or copy-pasted from the store side? The store mask only needs to answer "is the shorter encoding a net win with no correctness/perf downside," which is basically always true since there's no consumer to penalize. The load mask needs to answer a harder, per-microarchitecture question: "does this specific core charge zero cycles when an integer-domain instruction reads a register last written by movaps/movups." Those aren't the same claim. The domain-bypass delay for mixing float-tagged and int-tagged SSE moves is historically most persistent on the small/in-order-derived Atom line — Bonnell-generation Atom had a real, documented bypass cost for this exact pattern. Uros. > + > /* X86_TUNE_SSE_LOAD0_BY_PXOR: Always use pxor to load0 as opposed to > xorps/xorpd and other variants. */ > DEF_TUNE (X86_TUNE_SSE_LOAD0_BY_PXOR, "sse_load0_by_pxor", > diff --git a/gcc/testsuite/gcc.target/i386/sse-typeless-loads.c > b/gcc/testsuite/gcc.target/i386/sse-typeless-loads.c > new file mode 100644 > index 00000000000..ce1b28d2f45 > --- /dev/null > +++ b/gcc/testsuite/gcc.target/i386/sse-typeless-loads.c > @@ -0,0 +1,28 @@ > +/* Verify X86_TUNE_SSE_TYPELESS_LOADS: on tunings that use typeless 128-bit > + stores, a 128-bit load should likewise use the one-byte-shorter > movups/movaps > + rather than movdqu/movdqa (which differ only by a 66/F3 prefix). */ > +/* { dg-do compile } */ > +/* { dg-options "-O2 -msse2 -mno-avx -mtune=generic" } */ > + > +struct S16 { char c[16]; }; > + > +/* Unaligned copy: movdqu load -> movups (matches the movups store). */ > +void > +copy_unaligned (struct S16 *d, const struct S16 *s) > +{ > + *d = *s; > +} > + > +typedef int v4si __attribute__ ((vector_size (16))); > + > +/* Aligned copy: movdqa load -> movaps (matches the movaps store). */ > +void > +copy_aligned (v4si *d, const v4si *s) > +{ > + *d = *s; > +} > + > +/* { dg-final { scan-assembler-not "movdqu" } } */ > +/* { dg-final { scan-assembler-not "movdqa" } } */ > +/* { dg-final { scan-assembler "movups" } } */ > +/* { dg-final { scan-assembler "movaps" } } */ > -- > 2.55.0 >
