RE: [PATCH v3 1/1] aarch64: Avoid extra move for two-element vector lane copies [PR123951]

Tamar Christina <[email protected]>
Newsgroups gmane.comp.gcc.patches
Message-ID <VI0PR08MB10392EE44126DA871EE103DB0FFDE2@VI0PR08MB10392.eurprd08.prod.outlook.com>
> -----Original Message-----
> From: Rohith Kapelli <[email protected]>
> Sent: 07 August 2026 19:34
> To: [email protected]
> Cc: Richard Sandiford <[email protected]>; Tamar Christina
> <[email protected]>; Kyrylo Tkachov <[email protected]>;
> Richard Earnshaw <[email protected]>
> Subject: [PATCH v3 1/1] aarch64: Avoid extra move for two-element vector
> lane copies [PR123951]
> 
> Since r14-3381 the vec_extract/vec_set idiom used by the vcopy*_lane*
> intrinsics is folded to a VEC_PERM_EXPR, so the constant permute
> expander decides what to emit for it.  For a two-element vector the
> result takes one lane from each input, but the patterns that matched it
> tied the result to one particular input, and for half the lane
> combinations that is the wrong one, so the register allocator has to add
> a move.  On big endian arm_neon.h's lane flip puts
> vcopyq_laneq_u64 (a, 1, b, 1) in that half, so it regressed from one
> instruction to two and vect_copy_lane_1.c and vget_set_lane_1.c fail;
> little endian has the same problem on lane 0.
> 
> Add two patterns for the two-element modes, each offering a second
> alternative that ties the destination to the other input, so that the
> register allocator can insert into whichever input already occupies the
> destination.  The first handles a lane copy whose source and destination
> lanes are the same; the second handles a lane insert from a scalar,
> which always sits in the low part of its register, so it is restricted
> to the case where the destination is architectural lane 0.  Both sit
> before the corresponding general patterns, which continue to handle
> every other case, including cross-lane copies.
> 
> Both lane combinations of vcopy_lane_{s,u,f}32 and
> vcopyq_laneq_{s,u,f}64 now expand to a single INS on both endiannesses.
> The double add/sub merge in addsub_{1,2}.c uses the same two-element
> merge; with the tie left to the allocator its e1 case now inserts into
> lane 0 rather than lane 1, still a single INS, so the expected output is
> updated.
> 
> 	PR tree-optimization/123951
> 
> gcc/ChangeLog:
> 
> 	* config/aarch64/aarch64-simd.md
> 	(*aarch64_simd_vec_set_lane0<mode>): New pattern.
> 	(*aarch64_simd_vec_copy_lane_same<mode>): New pattern.
> 
> gcc/testsuite/ChangeLog:
> 
> 	* gcc.target/aarch64/pr123951_1.c: New test.
> 	* gcc.target/aarch64/pr123951_2.c: New test.
> 	* gcc.target/aarch64/simd/addsub_1.c: Update e1 lane insert from
> 	d[1] to d[0].
> 	* gcc.target/aarch64/simd/addsub_2.c: Likewise.
> 
> Signed-off-by: Rohith Kapelli <[email protected]>
> ---
> 
> > It looks like both patterns could use the "@..." syntax to specify the
> > alternatives, rather than switching on which_alternative.  There isn't
> > the ENDIAN_LANE_N stuff that makes some of the other patterns require
> > C++ code.
> >
> > The patterns could then use the new alternative syntax, where the
> > constraints and attributes are on the same line as the asm.
> 
> Done, both of them.  You're right that nothing here needs the C++: the
> lane numbers in the templates are constants, and the only ENDIAN_LANE_N
> use is in the insn condition, which is unchanged.  v3 converts both
> patterns to the "@" form with the constraints and attributes alongside
> each alternative, which also lets the per-operand constraint strings and
> the separate set_attr blocks go.  54 added lines rather than 68.
> 
> One thing worth flagging: the cons: list names operands 0, 1 and 3, so
> the two immediates lose their "i".  That is deliberate.  Comparing the
> genoutput data for v2 and v3, every register and memory constraint is
> byte-identical and only the two immediates change to "", which is what
> *cmov<mode>_insn already does for the operands its own cons: list skips.
> Their predicate is immediate_operand, so "i" was not adding anything.
> 
> The generated code is unchanged.  I compared the assembly out of the v2
> and v3 compilers for the two new tests and for a file covering every
> VP_2E mode (s64/u64/f64 and s32/u32/f32, both lane pairs and both
> directions) plus the vec_set paths (scalar from an FP register, from a
> general register and from memory), on aarch64-linux-gnu and
> aarch64_be-linux-gnu: byte-identical in all six comparisons.
> 
> Bootstrapped and regression tested on aarch64-unknown-linux-gnu, patched
> and unpatched built from clean trees in the same environment, both
> stage2 == stage3.  Comparing every result line across gcc, g++,
> libstdc++, libgomp, libitm and libatomic, the two runs are identical
> except for 11 new PASSes, which are the two new tests.  No PASS->FAIL.
> 
> addsub_{1,2}.c do still need the expectation change: with the patch, e1
> comes out as ins v.d[0], v.d[0], so the updated files pass and the
> d[1] versions fail on that one function and nothing else.
> 
> Also tested aarch64.exp and simd/simd.exp as crosses on both
> endiannesses, v2 against v3: all four .sum files identical.
> 
> > Otherwise this LGTM, but I'm not sure that I should approve.
> 
> Thanks.  Tamar, since the two-pattern shape is your suggestion, would you
> be happy to confirm the approval?
> 

OK.

Thanks,
Tamar

> Thanks,
> Rohith
> 
>  gcc/config/aarch64/aarch64-simd.md            | 54 +++++++++++++++++++
>  gcc/testsuite/gcc.target/aarch64/pr123951_1.c | 41 ++++++++++++++
>  gcc/testsuite/gcc.target/aarch64/pr123951_2.c | 35 ++++++++++++
>  .../gcc.target/aarch64/simd/addsub_1.c        |  2 +-
>  .../gcc.target/aarch64/simd/addsub_2.c        |  2 +-
>  5 files changed, 132 insertions(+), 2 deletions(-)
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/pr123951_1.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/pr123951_2.c
> 
> diff --git a/gcc/config/aarch64/aarch64-simd.md
> b/gcc/config/aarch64/aarch64-simd.md
> index ce51e24da36..80d8474b474 100644
> --- a/gcc/config/aarch64/aarch64-simd.md
> +++ b/gcc/config/aarch64/aarch64-simd.md
> @@ -1330,6 +1330,32 @@
>    [(set_attr "type" "neon_logic<q>")]
>  )
> 
> +;; Lane insert for the two-element modes.  A scalar source always occupies
> +;; the low part of its register, architectural lane 0.  When the inserted
> +;; element is that same lane, the destination can equally be tied to the
> +;; source register and the other lane brought in from operand 3, so offer
> +;; that as a second alternative and let the register allocator pick
> +;; whichever input already occupies the destination.  The remaining
> +;; alternatives are those of the general pattern below, so that a scalar
> +;; from a general register or from memory is unaffected.
> +(define_insn "*aarch64_simd_vec_set_lane0<mode>"
> +  [(set (match_operand:VP_2E 0 "register_operand")
> +	(vec_merge:VP_2E
> +	    (vec_duplicate:VP_2E
> +	      (match_operand:<VEL> 1
> "aarch64_simd_nonimmediate_operand"))
> +	    (match_operand:VP_2E 3 "register_operand")
> +	    (match_operand:SI 2 "immediate_operand")))]
> +  "TARGET_SIMD && INTVAL (operands[2]) == (BYTES_BIG_ENDIAN ? 2 : 1)"
> +  ;; In the second alternative the destination is the scalar's own register,
> +  ;; which already holds it in lane 0, so the other lane comes from operand 3.
> +  {@ [ cons: =0 , 1   , 3 ; attrs: type            ]
> +     [ w        , w   , 0 ; neon_ins<q>            ] ins\t%0.<Vetype>[0], %1.<Vetype>[0]
> +     [ w        , 0   , w ; neon_ins<q>            ] ins\t%0.<Vetype>[1], %3.<Vetype>[1]
> +     [ w        , ?r  , 0 ; neon_from_gp<q>        ] ins\t%0.<Vetype>[0], %<vwcore>1
> +     [ w        , Utv , 0 ; neon_load1_one_lane<q> ] ld1\t{%0.<Vetype>}[0], %1
> +  }
> +)
> +
>  (define_insn "@aarch64_simd_vec_set<mode>"
>    [(set (match_operand:VALL_F16 0 "register_operand" "=w,w,w")
>  	(vec_merge:VALL_F16
> @@ -1401,6 +1427,34 @@
>    }
>  )
> 
> +;; Lane copy between two two-element vectors.  When the source and
> +;; destination lanes are the same, the copy reads one lane from each input,
> +;; so it can equally be done by inserting the live lane of either input into
> +;; the other.  Offer both directions and let the register allocator tie
> +;; whichever input already occupies the destination, so that neither lane
> +;; needs an extra move.  Other lane combinations are left to the general
> +;; pattern below, where only the destination can be tied.
> +(define_insn "*aarch64_simd_vec_copy_lane_same<mode>"
> +  [(set (match_operand:VP_2E 0 "register_operand")
> +	(vec_merge:VP_2E
> +	    (vec_duplicate:VP_2E
> +	      (vec_select:<VEL>
> +		(match_operand:VP_2E 3 "register_operand")
> +		(parallel
> +		  [(match_operand:SI 4 "immediate_operand")])))
> +	    (match_operand:VP_2E 1 "register_operand")
> +	    (match_operand:SI 2 "immediate_operand")))]
> +  "TARGET_SIMD
> +   && ENDIAN_LANE_N (2, INTVAL (operands[4])) == 1
> +   && INTVAL (operands[2]) == (BYTES_BIG_ENDIAN ? 1 : 2)"
> +  ;; In the second alternative the destination is operand 3, which already
> +  ;; holds lane 1, so lane 0 comes from operand 1.
> +  {@ [ cons: =0 , 1 , 3 ; attrs: type ]
> +     [ w        , 0 , w ; neon_ins<q> ] ins\t%0.<Vetype>[1], %3.<Vetype>[1]
> +     [ w        , w , 0 ; neon_ins<q> ] ins\t%0.<Vetype>[0], %1.<Vetype>[0]
> +  }
> +)
> +
>  (define_insn "@aarch64_simd_vec_copy_lane<mode>"
>    [(set (match_operand:VALL_F16 0 "register_operand" "=w")
>  	(vec_merge:VALL_F16
> diff --git a/gcc/testsuite/gcc.target/aarch64/pr123951_1.c
> b/gcc/testsuite/gcc.target/aarch64/pr123951_1.c
> new file mode 100644
> index 00000000000..3c195505a89
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/pr123951_1.c
> @@ -0,0 +1,41 @@
> +/* PR tree-optimization/123951.  Copying a lane between two vectors must
> +   remain a single INS (or ZIP) whichever lane pair is used, on both
> +   endiannesses.  */
> +/* { dg-do compile } */
> +/* { dg-options "-O2" } */
> +
> +#include <arm_neon.h>
> +
> +#define BUILD_TEST(TYPE, Q1, Q2, SUFFIX, INDEX1, INDEX2)		\
> +TYPE __attribute__((noinline,noclone))
> 	\
> +test_copy##Q1##_lane##Q2##_##SUFFIX##_##INDEX1##INDEX2 (TYPE a,
> TYPE b) \
> +{									\
> +  return vcopy##Q1##_lane##Q2##_##SUFFIX (a, INDEX1, b, INDEX2);	\
> +}
> +
> +BUILD_TEST (uint64x2_t,  q, q, u64, 0, 0)
> +BUILD_TEST (int64x2_t,   q, q, s64, 0, 0)
> +BUILD_TEST (float64x2_t, q, q, f64, 0, 0)
> +/* { dg-final { scan-assembler-times "ins\\tv0.d\\\[0\\\], v1.d\\\[0\\\]" 3 } }
> */
> +BUILD_TEST (uint64x2_t,  q, q, u64, 1, 1)
> +BUILD_TEST (int64x2_t,   q, q, s64, 1, 1)
> +BUILD_TEST (float64x2_t, q, q, f64, 1, 1)
> +/* { dg-final { scan-assembler-times "ins\\tv0.d\\\[1\\\], v1.d\\\[1\\\]" 3 } }
> */
> +BUILD_TEST (uint64x2_t,  q, q, u64, 1, 0)
> +BUILD_TEST (int64x2_t,   q, q, s64, 1, 0)
> +BUILD_TEST (float64x2_t, q, q, f64, 1, 0)
> +/* { dg-final { scan-assembler-times "zip1\\tv0.2d, v0.2d, v1.2d" 3 } } */
> +BUILD_TEST (uint64x2_t,  q, q, u64, 0, 1)
> +BUILD_TEST (int64x2_t,   q, q, s64, 0, 1)
> +BUILD_TEST (float64x2_t, q, q, f64, 0, 1)
> +/* { dg-final { scan-assembler-times "zip2\\tv0.2d, v1.2d, v0.2d" 3 } } */
> +BUILD_TEST (uint32x2_t,  , , u32, 0, 0)
> +BUILD_TEST (int32x2_t,   , , s32, 0, 0)
> +BUILD_TEST (float32x2_t, , , f32, 0, 0)
> +/* { dg-final { scan-assembler-times "ins\\tv0.s\\\[0\\\], v1.s\\\[0\\\]" 3 } }
> */
> +BUILD_TEST (uint32x2_t,  , , u32, 1, 1)
> +BUILD_TEST (int32x2_t,   , , s32, 1, 1)
> +BUILD_TEST (float32x2_t, , , f32, 1, 1)
> +/* { dg-final { scan-assembler-times "ins\\tv0.s\\\[1\\\], v1.s\\\[1\\\]" 3 } }
> */
> +
> +/* { dg-final { scan-assembler-not "\\tmov\\t" } } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/pr123951_2.c
> b/gcc/testsuite/gcc.target/aarch64/pr123951_2.c
> new file mode 100644
> index 00000000000..59770b6b000
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/pr123951_2.c
> @@ -0,0 +1,35 @@
> +/* PR tree-optimization/123951.  Like pr123951_1.c, but for generic vector
> +   shuffles, including ones that only become lane inserts after being
> +   re-encoded to a wider element mode.  */
> +/* { dg-do compile } */
> +/* { dg-options "-O2" } */
> +
> +typedef unsigned long long v2di __attribute__((vector_size (16)));
> +typedef unsigned int v4si __attribute__((vector_size (16)));
> +
> +v2di
> +shuffle_03 (v2di a, v2di b)
> +{
> +  return __builtin_shuffle (a, b, (v2di) { 0, 3 });
> +}
> +
> +v2di
> +shuffle_21 (v2di a, v2di b)
> +{
> +  return __builtin_shuffle (a, b, (v2di) { 2, 1 });
> +}
> +
> +v4si
> +shuffle_0167 (v4si a, v4si b)
> +{
> +  return __builtin_shuffle (a, b, (v4si) { 0, 1, 6, 7 });
> +}
> +
> +v4si
> +shuffle_4523 (v4si a, v4si b)
> +{
> +  return __builtin_shuffle (a, b, (v4si) { 4, 5, 2, 3 });
> +}
> +
> +/* { dg-final { scan-assembler-times "\\tins\\t" 4 } } */
> +/* { dg-final { scan-assembler-not "\\tmov\\t" } } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/simd/addsub_1.c
> b/gcc/testsuite/gcc.target/aarch64/simd/addsub_1.c
> index 1fb91a34c42..5acfe33a576 100644
> --- a/gcc/testsuite/gcc.target/aarch64/simd/addsub_1.c
> +++ b/gcc/testsuite/gcc.target/aarch64/simd/addsub_1.c
> @@ -43,7 +43,7 @@ void d1 (_Float16 *restrict a, _Float16 *restrict b,
> _Float16 *res, int n)
>  ** ...
>  ** 	fadd	v[0-9]+.2d, v[0-9]+.2d, v[0-9]+.2d
>  ** 	fsub	v[0-9]+.2d, v[0-9]+.2d, v[0-9]+.2d
> -** 	ins	v[0-9]+.d\[1\], v[0-9]+.d\[1\]
> +** 	ins	v[0-9]+.d\[0\], v[0-9]+.d\[0\]
>  ** ...
>  */
>  void e1 (double *restrict a, double *restrict b, double *res, int n)
> diff --git a/gcc/testsuite/gcc.target/aarch64/simd/addsub_2.c
> b/gcc/testsuite/gcc.target/aarch64/simd/addsub_2.c
> index 87424c94f24..023adb46ece 100644
> --- a/gcc/testsuite/gcc.target/aarch64/simd/addsub_2.c
> +++ b/gcc/testsuite/gcc.target/aarch64/simd/addsub_2.c
> @@ -43,7 +43,7 @@ void d1 (_Float16 *restrict a, _Float16 *restrict b,
> _Float16 *res, int n)
>  ** ...
>  ** 	fsub	v[0-9]+.2d, v[0-9]+.2d, v[0-9]+.2d
>  ** 	fadd	v[0-9]+.2d, v[0-9]+.2d, v[0-9]+.2d
> -** 	ins	v[0-9]+.d\[1\], v[0-9]+.d\[1\]
> +** 	ins	v[0-9]+.d\[0\], v[0-9]+.d\[0\]
>  ** ...
>  */
>  void e1 (double *restrict a, double *restrict b, double *res, int n)
> --
> 2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.