Hi Hongtao,
Many thanks for the review and suggested improvements.
This revision implements Hongtao's suggestions/fixes to support
TImode values in non-general hard registers, and adds two more
test cases. Alas things turned out to be a little more complicated
than originally proposed; previously STV used PUT_MODE on TImode
pseudo registers to change their mode everywhere, but something
different is required for hard registers, which may be used in
multiple modes in a function.
To demonstrate the (additional) benefits, consider the function:
register __int128 x __asm("xmm0");
register __int128 y __asm("xmm1");
__int128 m;
void foo()
{
m = x ^ y;
}
Previously GCC on x86_64 with -O2 generated:
foo: movaps %xmm0, -24(%rsp)
movq -24(%rsp), %rax
movq -16(%rsp), %rdx
movaps %xmm1, -24(%rsp)
xorq -24(%rsp), %rax
xorq -16(%rsp), %rdx
movq %rax, m(%rip)
movq %rdx, m+8(%rip)
ret
With this revised patch, we now generate:
foo: movdqa %xmm0, %xmm2
pxor %xmm1, %xmm2
movaps %xmm2, m(%rip)
ret
This patch has been tested on x86_64-pc-linux-gnu with make bootstrap
and make -k check, both with and without --target_board=unix{-m32}
with no new failures.
2026-05-14 Roger Sayle <[email protected]>
Hongtao Liu <[email protected]>
gcc/ChangeLog
* config/i386/i386-features.cc (scalar_chain): If the chain
starts with a register-to-register move from a hard register,
then the hard register's defs don't need to converted.
(timode_scalar_chain::compute_convert_gain): Provide costs
for hard_reg-to-pseudo and pseudo-to-hard_reg moves.
Tweak speed cost of timode_concatdi_p moves.
(timode_scalar_chain::convert_insn): Add support for
hard_reg-to-pseudo and pseudo-to-hard_reg TImode transfers.
(timode_scalar_to_vector_candidate_p): Likewise.
gcc/testsuite/ChangeLog
* gcc.target/i386/avx-stv-1.c: New test case.
* gcc.target/i386/sse2-stv-3.c: Likewise.
* gcc.target/i386/sse2-stv-4.c: Likewise.
* gcc.target/i386/sse2-stv-5.c: Likewise.
Thanks again,
Roger
--
> -----Original Message-----
> From: Hongtao Liu <[email protected]>
> Sent: 05 May 2026 02:44
> To: Roger Sayle <[email protected]>
> Cc: GCC Patches <[email protected]>; Liu, Hongtao
> <[email protected]>; Uros Bizjak <[email protected]>
> Subject: Re: [x86_64 PATCH] Handle hard registers in STV with inter-unit
> moves.
>
> On Tue, May 5, 2026 at 9:30 AM Hongtao Liu <[email protected]> wrote:
> >
> > On Sun, May 3, 2026 at 6:58 AM Roger Sayle <[email protected]>
> wrote:
> > >
> > >
> > > This patch extends the types of chains that can be converted by
> > > x86's TImode Scalar-To-Vector (STV) pass, to include chains that
> > > originate and/or terminate with moves from/to hard registers.
> > > Currently STV candidate instructions explicitly exclude those than
> > > mention hard registers.
> > >
> > > As motivation, consider the four following functions:
> > >
> > > __int128 a, b, c, z;
> > > __int128 fun();
> > >
> > > void foo_in(__int128 x) { z = (x ^ a ^ b ^ c); }
> > >
> > > __int128 foo_out() { return (z ^ a ^ b ^ c); }
> > >
> > > __int128 foo_inout(__int128 x) { return (x ^ a ^ b ^ c ^ z); }
> > >
> > > void foo_fun() { z = (fun() ^ a ^ b ^ c); }
> > >
> > > Of these, only the first, foo_in, is currently STV converted to use
> > > SSE instructions. Its incoming argument is constructed from a
> > > concat of two DImode registers, and support for this idiom was added
> > > in a previous STV patch. The next two functions aren't converted
> > > because the chain terminates with a return, which places the TImode
> > > result in a hard register. Likewise, the final foo_fun case isn't
> > > converted as the result from fun initiates a chain from a hard register.
> > >
> > > This patch supports STV conversion of TImode register-to-register
> > > moves, where either the source or the destination (but not both) is
> > > a hard register, by implementing it as a (relatively expensive)
> > > inter-unit move.
> > >
> > > Before, with -O2 -mavx:
> > >
> > > foo_out:
> > > movq z(%rip), %rax
> > > movq z+8(%rip), %rdx
> > > xorq a(%rip), %rax
> > > xorq a+8(%rip), %rdx
> > > xorq b(%rip), %rax
> > > xorq b+8(%rip), %rdx
> > > xorq c(%rip), %rax
> > > xorq c+8(%rip), %rdx
> > > ret
> > >
> > > After, with -O2 -mavx:
> > >
> > > foo_out:
> > > vmovdqa z(%rip), %xmm0
> > > vpxor a(%rip), %xmm0, %xmm0
> > > vpxor b(%rip), %xmm0, %xmm0
> > > vpxor c(%rip), %xmm0, %xmm0
> > > vpextrq $1, %xmm0, %rdx
> > > vmovq %xmm0, %rax
> > > ret
> > >
> > > Likewise for foo_fun, before with -O2 -mavx:
> > >
> > > foo_fun:
> > > subq $8, %rsp
> > > call fun
> > > movq a(%rip), %rsi
> > > movq a+8(%rip), %rdi
> > > xorq b(%rip), %rsi
> > > xorq b+8(%rip), %rdi
> > > xorq c(%rip), %rsi
> > > xorq c+8(%rip), %rdi
> > > xorq %rax, %rsi
> > > xorq %rdx, %rdi
> > > movq %rsi, z(%rip)
> > > movq %rdi, z+8(%rip)
> > > addq $8, %rsp
> > > ret
> > >
> > > After with -O2 -mavx:
> > >
> > > foo_fun:
> > > subq $8, %rsp
> > > call fun
> > > vmovdqa a(%rip), %xmm0
> > > vpxor b(%rip), %xmm0, %xmm0
> > > vmovq %rax, %xmm2
> > > vpxor c(%rip), %xmm0, %xmm0
> > > vpinsrq $1, %rdx, %xmm2, %xmm1
> > > vpxor %xmm1, %xmm0, %xmm0
> > > vmovdqa %xmm0, z(%rip)
> > > addq $8, %rsp
> > > ret
> > >
> > > The one small subtlety in this patch is in the cost calculation for
> > > inter-unit moves, which now correctly uses both sse_to_integer and
> > > integer_to_sse costs. This patch models the transfer of double word
> > > transfers between units as interunit_cost + COSTS_N_INSNS(1), i.e.
> > > that the two transfers are pipelined in parallel, so that the high
> > > latency is accounted for once [rather than 2*interunit_cost that
> > > assumes the transfers take place strictly sequentially with twice
> > > the single word transfer latency].
> > >
> > >
> > > This patch has been tested on x86_64-pc-linux-gnu with make
> > > bootstrap and make -k check, both with and without
> > > --target_board=unix{-m32} with no new failures. Ok for mainline?
> > >
> > >
> > > 2026-05-02 Roger Sayle <[email protected]>
> > >
> > > gcc/ChangeLog
> > > * config/i386/i386-features.cc (scalar_chain): If the chain
> > > starts with a register-to-register move from a hard register,
> > > then the hard register's defs don't need to converted.
> > > (timode_scalar_chain::compute_convert_gain): Provide costs
> > > for hard_reg-to-pseudo and pseudo-to-hard_reg moves.
> > > Tweak speed cost of timode_concatdi_p moves.
> > > (timode_scalar_chain::convert_insn): Add support for
> > > hard_reg-to-pseudo and pseudo-to-hard_reg TImode transfers.
> > > (timode_scalar_to_vector_candidate_p): Likewise.
> > >
> > > gcc/testsuite/ChangeLog
> > > * gcc.target/i386/avx-stv-1.c: New test case.
> > > * gcc.target/i386/sse2-stv-3.c: Likewise.
> > >
> >
> >
> > >@@ -1961,7 +1991,8 @@ timode_scalar_chain::convert_insn (rtx_insn
> > *insn)
> > > switch (GET_CODE (dst))
> > > {
> > > case REG:
> > >- if (GET_MODE (dst) == TImode)
> > >+ if (GET_MODE (dst) == TImode
> > >+ && !HARD_REGISTER_P (dst))
> > I think it should be !GENERAL_REGNO_P (REGNO (dst)), there's will be
> > ICE for
> Just GENERAL_REG_P (dst).
>
> > void foo_out1() {
> > register __int128 x __asm("xmm0");
> > z = (x ^ a ^ b ^ c);
> > }
> >
> > Similar for the change in compute_convert_gain and below
> >
> > > {
> > > PUT_MODE (dst, V1TImode);
> > > fix_debug_reg_uses (dst);
> > >@@ -1988,8 +2019,40 @@ timode_scalar_chain::convert_insn (rtx_insn
> *insn)
> > > case REG:
> > > if (GET_MODE (src) == TImode)
> > > {
> > >- PUT_MODE (src, V1TImode);
> > >- fix_debug_reg_uses (src);
> > >+ if (HARD_REGISTER_P (src))
> > Ditto here.
> > >+ {
> > >+ rtx lo = gen_reg_rtx (DImode);
> > >+ rtx hi = gen_reg_rtx (DImode);
> > >+ emit_insn_before (gen_rtx_SET (lo, gen_lowpart (DImode, src)),
> > >+ insn);
> > >+ emit_insn_before (gen_rtx_SET (hi, gen_highpart (DImode,
> > >src)),
> > >+ insn);
> > >+ src = gen_reg_rtx (V2DImode);
> > >+ emit_insn_before (gen_vec_concatv2di (src, lo, hi), insn);
> > >+ src = gen_lowpart (V1TImode, src);
> > >+ }
> > >+ else
> > >+ {
> > >+ PUT_MODE (src, V1TImode);
> > >+ fix_debug_reg_uses (src);
> > >+ }
> > >+ }
> > >+ if (REG_P (dst) && HARD_REGISTER_P (dst))
> > Ditto here.
> >
> > >+ {
> > >+ rtx tmp = gen_reg_rtx (V2DImode);
> > >+ src = gen_lowpart (V2DImode, src);
> > >+ emit_insn_before (gen_rtx_SET (tmp, src), insn);
> > >+ /* Extracting hi before lo helps register allocation. */
> > >+ rtx hi = gen_reg_rtx (DImode);
> > >+ rtx lo = gen_reg_rtx (DImode);
> > >+ emit_insn_before (gen_vec_extractv2didi (hi, tmp, const1_rtx),
> > >insn);
> > >+ emit_insn_before (gen_vec_extractv2didi (lo, tmp,
> > >+ const0_rtx), insn);
> > >+
> > >+ /* Construct *concatditi3 pattern from lo and hi. */
> > >+ hi = gen_rtx_ZERO_EXTEND (TImode, hi);
> > >+ hi = gen_rtx_ASHIFT (TImode, hi, GEN_INT (64));
> > >+ lo = gen_rtx_ZERO_EXTEND (TImode, lo);
> > >+ src = gen_rtx_PLUS (TImode, hi, lo);
> > > }
> > > break;
> > >
> >
> > But for timode_scalar_to_vector_candidate_p, I think we can keep
> > HARD_REGISTER_P, since xmm0 should also be ok.
> > >
> > > Thanks,
> > > Roger
> > > --
> > >
> >
> >
> > --
> > BR,
> > Hongtao
>
>
>
> --
> BR,
> Hongtao
diff --git a/gcc/config/i386/i386-features.cc b/gcc/config/i386/i386-features.cc
index ce5f0e9c178..9381674950f 100644
--- a/gcc/config/i386/i386-features.cc
+++ b/gcc/config/i386/i386-features.cc
@@ -506,6 +506,10 @@ scalar_chain::add_insn (bitmap candidates, unsigned int
insn_uid,
if (def_set)
switch (GET_CODE (SET_SRC (def_set)))
{
+ case REG:
+ if (HARD_REGISTER_P (SET_SRC (def_set)))
+ return true;
+ break;
case VEC_SELECT:
return true;
case ZERO_EXTEND:
@@ -1641,7 +1645,33 @@ timode_scalar_chain::compute_convert_gain ()
switch (GET_CODE (src))
{
case REG:
- if (!speed_p)
+ if (GENERAL_REGNO_P (REGNO (src)))
+ {
+ if (TARGET_AVX)
+ /* vmovq + vpinsrq */
+ igain = speed_p ? -ix86_cost->integer_to_sse
+ - COSTS_N_INSNS (1)
+ : -COSTS_N_BYTES (11);
+ else
+ /* movq + movq + punpcklqdq */
+ igain = speed_p ? -ix86_cost->integer_to_sse
+ - COSTS_N_INSNS (2)
+ : -COSTS_N_BYTES (14);
+ }
+ else if (GENERAL_REG_P (dst))
+ {
+ if (TARGET_AVX)
+ /* vpextrq + vmovq */
+ igain = speed_p ? -ix86_cost->sse_to_integer
+ - COSTS_N_INSNS (1)
+ : -COSTS_N_BYTES (11);
+ else
+ /* movhlps + movq + movq */
+ igain = speed_p ? -ix86_cost->sse_to_integer
+ - COSTS_N_INSNS (2)
+ : -COSTS_N_BYTES (13);
+ }
+ else if (!speed_p)
igain = MEM_P (dst) ? COSTS_N_BYTES (6) : COSTS_N_BYTES (3);
else
igain = COSTS_N_INSNS (1);
@@ -1680,7 +1710,7 @@ timode_scalar_chain::compute_convert_gain ()
if (timode_concatdi_p (src))
{
/* vmovq;vpinsrq (11 bytes). */
- igain = speed_p ? -2 * ix86_cost->sse_to_integer
+ igain = speed_p ? -ix86_cost->integer_to_sse - COSTS_N_INSNS (1)
: -COSTS_N_BYTES (11);
break;
}
@@ -1693,7 +1723,7 @@ timode_scalar_chain::compute_convert_gain ()
case PLUS:
if (timode_concatdi_p (src))
/* vmovq;vpinsrq (11 bytes). */
- igain = speed_p ? -2 * ix86_cost->sse_to_integer
+ igain = speed_p ? -ix86_cost->integer_to_sse - COSTS_N_INSNS (1)
: -COSTS_N_BYTES (11);
break;
@@ -1963,8 +1993,13 @@ timode_scalar_chain::convert_insn (rtx_insn *insn)
case REG:
if (GET_MODE (dst) == TImode)
{
- PUT_MODE (dst, V1TImode);
- fix_debug_reg_uses (dst);
+ if (!HARD_REGISTER_NUM_P (REGNO (dst)))
+ {
+ PUT_MODE (dst, V1TImode);
+ fix_debug_reg_uses (dst);
+ }
+ else if (!GENERAL_REGNO_P (REGNO (dst)))
+ dst = gen_raw_REG (V1TImode, REGNO (dst));
}
if (GET_MODE (dst) == V1TImode)
{
@@ -1988,8 +2023,42 @@ timode_scalar_chain::convert_insn (rtx_insn *insn)
case REG:
if (GET_MODE (src) == TImode)
{
- PUT_MODE (src, V1TImode);
- fix_debug_reg_uses (src);
+ if (GENERAL_REGNO_P (REGNO (src)))
+ {
+ rtx lo = gen_reg_rtx (DImode);
+ rtx hi = gen_reg_rtx (DImode);
+ emit_insn_before (gen_rtx_SET (lo, gen_lowpart (DImode, src)),
+ insn);
+ emit_insn_before (gen_rtx_SET (hi, gen_highpart (DImode, src)),
+ insn);
+ src = gen_reg_rtx (V2DImode);
+ emit_insn_before (gen_vec_concatv2di (src, lo, hi), insn);
+ src = gen_lowpart (V1TImode, src);
+ }
+ else if (!HARD_REGISTER_NUM_P (REGNO (src)))
+ {
+ PUT_MODE (src, V1TImode);
+ fix_debug_reg_uses (src);
+ }
+ else
+ src = gen_raw_REG (V1TImode, REGNO (src));
+ }
+ if (GENERAL_REG_P (dst))
+ {
+ rtx tmp = gen_reg_rtx (V2DImode);
+ src = gen_lowpart (V2DImode, src);
+ emit_insn_before (gen_rtx_SET (tmp, src), insn);
+ /* Extracting hi before lo helps register allocation. */
+ rtx hi = gen_reg_rtx (DImode);
+ rtx lo = gen_reg_rtx (DImode);
+ emit_insn_before (gen_vec_extractv2didi (hi, tmp, const1_rtx), insn);
+ emit_insn_before (gen_vec_extractv2didi (lo, tmp, const0_rtx), insn);
+
+ /* Construct *concatditi3 pattern from lo and hi. */
+ hi = gen_rtx_ZERO_EXTEND (TImode, hi);
+ hi = gen_rtx_ASHIFT (TImode, hi, GEN_INT (64));
+ lo = gen_rtx_ZERO_EXTEND (TImode, lo);
+ src = gen_rtx_PLUS (TImode, hi, lo);
}
break;
@@ -2453,8 +2522,31 @@ timode_scalar_to_vector_candidate_p (rtx_insn *insn)
{
rtx def_set = pseudo_reg_set (insn);
+ /* We allow two exceptions to the pseudo registers only rule.
+ Setting a hard register from a pseudo, and setting a pseudo
+ from a hard register. */
if (!def_set)
- return false;
+ {
+ def_set = single_set (insn);
+ if (def_set)
+ {
+ rtx src = SET_SRC (def_set);
+ rtx dst = SET_DEST (def_set);
+ if (GET_MODE (dst) == TImode
+ && REG_P (src) && REG_P (dst))
+ {
+ if (HARD_REGISTER_P (dst)
+ && !HARD_REGISTER_P (src)
+ && single_def_chain_p (src))
+ return true;
+ if (HARD_REGISTER_P (src)
+ && !HARD_REGISTER_P (dst)
+ && single_def_chain_p (dst))
+ return true;
+ }
+ }
+ return false;
+ }
rtx src = SET_SRC (def_set);
rtx dst = SET_DEST (def_set);
diff --git a/gcc/testsuite/gcc.target/i386/avx-stv-1.c
b/gcc/testsuite/gcc.target/i386/avx-stv-1.c
new file mode 100644
index 00000000000..e9dea2d0f4a
--- /dev/null
+++ b/gcc/testsuite/gcc.target/i386/avx-stv-1.c
@@ -0,0 +1,30 @@
+/* { dg-do compile { target int128 } } */
+/* { dg-options "-O2 -mavx -mno-stackrealign" } */
+
+__int128 a, b, c, z;
+
+__int128 fun();
+
+void foo_in(__int128 x)
+{
+ z = (x ^ a ^ b ^ c);
+}
+
+__int128 foo_out()
+{
+ return (z ^ a ^ b ^ c);
+}
+
+__int128 foo_inout(__int128 x)
+{
+ return (x ^ a ^ b ^ c ^ z);
+}
+
+void foo_fun()
+{
+ z = (fun() ^ a ^ b ^ c);
+}
+
+/* { dg-final { scan-assembler-times "vpinsrq" 3 } } */
+/* { dg-final { scan-assembler-times "vpextrq" 2 } } */
+/* { dg-final { scan-assembler-times "vpxor" 13 } } */
diff --git a/gcc/testsuite/gcc.target/i386/sse2-stv-3.c
b/gcc/testsuite/gcc.target/i386/sse2-stv-3.c
new file mode 100644
index 00000000000..0a638013aed
--- /dev/null
+++ b/gcc/testsuite/gcc.target/i386/sse2-stv-3.c
@@ -0,0 +1,31 @@
+/* { dg-do compile { target int128 } } */
+/* { dg-options "-O2 -msse2 -mno-sse4 -mno-stackrealign" } */
+
+__int128 a, b, c, z;
+
+__int128 fun();
+
+void foo_in(__int128 x)
+{
+ z = (x ^ a ^ b ^ c);
+}
+
+__int128 foo_out()
+{
+ return (z ^ a ^ b ^ c);
+}
+
+__int128 foo_inout(__int128 x)
+{
+ return (x ^ a ^ b ^ c ^ z);
+}
+
+void foo_fun()
+{
+ z = (fun() ^ a ^ b ^ c);
+}
+
+/* { dg-final { scan-assembler-times "punpcklqdq" 2 } } */
+/* { dg-final { scan-assembler-times "movhlps" 1 } } */
+/* { dg-final { scan-assembler-times "pxor" 9 } } */
+/* { dg-final { scan-assembler-times "xorq" 8 } } */
diff --git a/gcc/testsuite/gcc.target/i386/sse2-stv-4.c
b/gcc/testsuite/gcc.target/i386/sse2-stv-4.c
new file mode 100644
index 00000000000..8c655ff9a4b
--- /dev/null
+++ b/gcc/testsuite/gcc.target/i386/sse2-stv-4.c
@@ -0,0 +1,12 @@
+/* { dg-do compile { target int128 } } */
+/* { dg-options "-O2 -msse2 -mno-sse4 -mno-stackrealign" } */
+
+__int128 a, b, c, z;
+
+void foo()
+{
+ register __int128 x __asm("xmm0");
+ z = (x ^ a ^ b ^ c);
+}
+
+/* { dg-final { scan-assembler-times "pxor" 3 } } */
diff --git a/gcc/testsuite/gcc.target/i386/sse2-stv-5.c
b/gcc/testsuite/gcc.target/i386/sse2-stv-5.c
new file mode 100644
index 00000000000..b4bbc2bbae5
--- /dev/null
+++ b/gcc/testsuite/gcc.target/i386/sse2-stv-5.c
@@ -0,0 +1,13 @@
+/* { dg-do compile { target int128 } } */
+/* { dg-options "-O2 -msse2 -mno-sse4 -mno-stackrealign" } */
+
+__int128 m;
+
+void foo()
+{
+ register __int128 x __asm("xmm0");
+ register __int128 y __asm("xmm1");
+ m = x ^ y;
+}
+
+/* { dg-final { scan-assembler-times "pxor" 1 } } */