On Fri, Sep 11, 2026 at 6:12 AM <[email protected]> wrote:
>
> From: Kyrylo Tkachov <[email protected]>
>
> A signed value clamped to the range of a narrower unsigned type is unsigned
> saturating truncation.  Recognize nested MIN and MAX operations in either
> order.
>
> Require exact zero and maximum bounds.  Require the outer clamp to have one
> use so that the replacement removes it.  The inner clamp can have other uses.
>
> With AArch64 -O2 -march=armv8-a -fopenmp-simd:
>
> before:
>
> clamp_u16_4:
>         ldr     q0, [x1]
>         movi    v30.4s, 0xff, msl 8
>         movi    v31.4s, 0
>         smin    v30.4s, v0.4s, v30.4s
>         smax    v30.4s, v30.4s, v31.4s
>         xtn     v30.4h, v30.4s
>         str     d30, [x0]
>         ret
>
> after:
>
> clamp_u16_4:
>         ldr     q31, [x1]
>         movi    v30.4s, 0
>         smax    v30.4s, v31.4s, v30.4s
>         uqxtn   v30.4h, v30.4s
>         str     d30, [x0]
>         ret
>
> Runtime tests cover both clamp orders, two widths, and LTO.  Target tests
> cover valid bounds, invalid bounds, and extra uses of each clamp.
>
> Bootstrapped and tested on aarch64-none-linux-gnu.
>
> Ok for trunk?
>
> Changes since v2:
> - Rebase on current trunk.
>
> gcc/ChangeLog:
>
>         * match-sat-alu.pd (integer_constant_clamp): New match.
>         (unsigned_integer_narrow_clip): Add the constant-clamp form.
>
> gcc/testsuite/ChangeLog:
>
>         * gcc.dg/vect/vect-sat-trunc-clamp-1.c: New test.
>         * gcc.target/aarch64/vect-sat-trunc-clamp-inner-use-1.c: Likewise.
>         * gcc.target/aarch64/vect-sat-trunc-clamp-multi-use-1.c: Likewise.
>         * gcc.target/aarch64/vect-sat-trunc-clamp-u-1.c: Likewise.
>         * gcc.target/aarch64/vect-sat-trunc-clamp-u-negative-1.c: Likewise.
>
> Signed-off-by: Kyrylo Tkachov <[email protected]>
> ---
>  gcc/match-sat-alu.pd                          |  27 +++-
>  .../gcc.dg/vect/vect-sat-trunc-clamp-1.c      | 121 ++++++++++++++++++
>  .../vect-sat-trunc-clamp-inner-use-1.c        |  22 ++++
>  .../vect-sat-trunc-clamp-multi-use-1.c        |  21 +++
>  .../aarch64/vect-sat-trunc-clamp-u-1.c        |   7 +
>  .../vect-sat-trunc-clamp-u-negative-1.c       |  30 +++++
>  6 files changed, 227 insertions(+), 1 deletion(-)
>  create mode 100644 gcc/testsuite/gcc.dg/vect/vect-sat-trunc-clamp-1.c
>  create mode 100644 
> gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-inner-use-1.c
>  create mode 100644 
> gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-multi-use-1.c
>  create mode 100644 
> gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-1.c
>  create mode 100644 
> gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-negative-1.c
>
> diff --git a/gcc/match-sat-alu.pd b/gcc/match-sat-alu.pd
> index 5a5ed52d40d..e7bcb6861e7 100644
> --- a/gcc/match-sat-alu.pd
> +++ b/gcc/match-sat-alu.pd
> @@ -23,6 +23,16 @@ along with GCC; see the file COPYING3.  If not see
>  <http://www.gnu.org/licenses/>.  */
>
>
> +/* Clamp @0 between the constant bounds @1 and @2.  The source can put the
> +   MIN or the MAX first.  The outer operation must be single use so that
> +   replacing its conversion also removes the complete clamp.  */
> +(match (integer_constant_clamp @0 @1 @2)
> + (max (min @0 INTEGER_CST@2) INTEGER_CST@1)
> + (if (single_use (t))))
> +(match (integer_constant_clamp @0 @1 @2)
> + (min (max @0 INTEGER_CST@1) INTEGER_CST@2)
> + (if (single_use (t))))

(note I didn't realize `t` usage was documented and had to look it up).
Should both min and max be single_use here or is the outer one only needed?
Also should the use of integer_constant_clamp instead use single_use then?

> +
>  /* Saturation add for unsigned integer.  */
>  (if (INTEGRAL_TYPE_P (type) && TYPE_UNSIGNED (type))
>   (match (usadd_overflow_mask @0 @1)
> @@ -293,7 +303,22 @@ along with GCC; see the file COPYING3.  If not see
>          && wi::eq_p (trunc_max, int_cst_1)
>          && cmp >= 0
>          && expr_not_equal_to (@0, wi::min_value (itype_precision,
> -                                                 SIGNED))))))))
> +                                                 SIGNED)))))))
> + (match (unsigned_integer_narrow_clip @0)
> +  /* SAT_US_TRUNC = (NT)MAX (MIN (X, NT_MAX), 0).  */
> +  (convert (integer_constant_clamp @0 @1 @2))
> +  (if (!TYPE_UNSIGNED (TREE_TYPE (@0)))
> +   (with
> +    {
> +     unsigned itype_precision = TYPE_PRECISION (TREE_TYPE (@0));
> +     unsigned otype_precision = TYPE_PRECISION (type);
> +     wide_int trunc_max = wi::mask (otype_precision, false, itype_precision);
> +     wide_int lo_cst = wi::to_wide (@1, itype_precision);
> +     wide_int hi_cst = wi::to_wide (@2, itype_precision);
> +    }
> +    (if (otype_precision < itype_precision
> +        && wi::eq_p (lo_cst, 0)
> +        && wi::eq_p (hi_cst, trunc_max)))))))


Instead of lo_cst here, why not use integer_zerop on @1
That is:
(match (unsigned_integer_narrow_clip @0)
 /* SAT_US_TRUNC = (NT)MAX (MIN (X, NT_MAX), 0).  */
 (convert (integer_constant_clamp @0 integer_zerop@1 @2))
 (if (!TYPE_UNSIGNED (TREE_TYPE (@0)))

That might speed up things slightly and will produce smaller code.

>
>  /* Saturation truncate for unsigned integer.  */
>  (if (INTEGRAL_TYPE_P (type) && TYPE_UNSIGNED (type))
> diff --git a/gcc/testsuite/gcc.dg/vect/vect-sat-trunc-clamp-1.c 
> b/gcc/testsuite/gcc.dg/vect/vect-sat-trunc-clamp-1.c
> new file mode 100644
> index 00000000000..06a4428d0d2
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/vect/vect-sat-trunc-clamp-1.c
> @@ -0,0 +1,121 @@
> +/* { dg-do run } */
> +/* { dg-require-effective-target vect_int } */
> +/* { dg-additional-options "-O3 -fvect-cost-model=unlimited" } */
> +
> +#include "tree-vect.h"
> +
> +typedef __UINT16_TYPE__ u16;
> +typedef __UINT32_TYPE__ u32;
> +typedef __UINT64_TYPE__ u64;
> +typedef __INT32_TYPE__ i32;
> +typedef __INT64_TYPE__ i64;
> +
> +#define N 259
> +
> +static i32 in32[N];
> +static i64 in64[N];
> +static u16 out16[N];
> +static u32 out32[N];
> +
> +__attribute__((noipa)) static void
> +clamp_u16_lo (u16 *__restrict out, const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      out[i] = x < 0 ? 0 : (x > 65535 ? 65535 : x);
> +    }
> +}
> +
> +__attribute__((noipa)) static void
> +clamp_u16_hi (u16 *__restrict out, const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      out[i] = x > 65535 ? 65535 : (x < 0 ? 0 : x);
> +    }
> +}
> +
> +__attribute__((noipa)) static void
> +clamp_u32_lo (u32 *__restrict out, const i64 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i64 x = in[i];
> +      out[i] = x < 0 ? 0 : (x > 4294967295LL ? 4294967295LL : x);
> +    }
> +}
> +
> +__attribute__((noipa)) static void
> +clamp_u32_hi (u32 *__restrict out, const i64 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i64 x = in[i];
> +      out[i] = x > 4294967295LL ? 4294967295LL : (x < 0 ? 0 : x);
> +    }
> +}
> +
> +static u16
> +ref_u16 (i32 x)
> +{
> +  return x < 0 ? 0 : (x > 65535 ? 65535 : x);
> +}
> +
> +static u32
> +ref_u32 (i64 x)
> +{
> +  return x < 0 ? 0 : (x > 4294967295LL ? 4294967295LL : x);
> +}
> +
> +static void
> +check_u16 (void)
> +{
> +#pragma GCC novector
> +  for (int i = 0; i < N; ++i)
> +    if (out16[i] != ref_u16 (in32[i]))
> +      __builtin_abort ();
> +}
> +
> +static void
> +check_u32 (void)
> +{
> +#pragma GCC novector
> +  for (int i = 0; i < N; ++i)
> +    if (out32[i] != ref_u32 (in64[i]))
> +      __builtin_abort ();
> +}
> +
> +int
> +main (void)
> +{
> +  check_vect ();
> +
> +  for (int i = 0; i < N; ++i)
> +    {
> +      in32[i] = (i32) ((u32) i * 2654435761U + 1013904223U);
> +      in64[i] = (i64) ((u64) i * 11400714819323198485ULL
> +                            + 13787848793156543929ULL);
> +    }
> +
> +  in32[0] = -1;
> +  in32[1] = 0;
> +  in32[2] = 65535;
> +  in32[3] = 65536;
> +  in64[0] = -1;
> +  in64[1] = 0;
> +  in64[2] = 4294967295LL;
> +  in64[3] = 4294967296LL;
> +
> +  clamp_u16_lo (out16, in32, N);
> +  check_u16 ();
> +  clamp_u16_hi (out16, in32, N);
> +  check_u16 ();
> +  clamp_u32_lo (out32, in64, N);
> +  check_u32 ();
> +  clamp_u32_hi (out32, in64, N);
> +  check_u32 ();
> +
> +  return 0;
> +}
> diff --git 
> a/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-inner-use-1.c 
> b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-inner-use-1.c
> new file mode 100644
> index 00000000000..b93bc56188e
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-inner-use-1.c
> @@ -0,0 +1,22 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=armv8-a" } */
> +/* { dg-additional-options "-mmax-vectorization 
> --param=vect-epilogues-nomask=0 -fdump-tree-vect-details" } */
> +
> +typedef __UINT16_TYPE__ u16;
> +typedef __INT32_TYPE__ i32;
> +
> +void
> +clip (u16 *__restrict out, i32 *__restrict copy,
> +      const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      i32 inner = x > 65535 ? 65535 : x;
> +      i32 outer = inner < 0 ? 0 : inner;
> +      out[i] = outer;
> +      copy[i] = inner;
> +    }
> +}
> +
> +/* { dg-final { scan-tree-dump-times "sat_trunc pattern recognized" 1 "vect" 
> } } */
> diff --git 
> a/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-multi-use-1.c 
> b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-multi-use-1.c
> new file mode 100644
> index 00000000000..0b82d40ff2f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-multi-use-1.c
> @@ -0,0 +1,21 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=armv8-a" } */
> +/* { dg-additional-options "-mmax-vectorization 
> --param=vect-epilogues-nomask=0 -fdump-tree-vect-details" } */
> +
> +typedef __UINT16_TYPE__ u16;
> +typedef __INT32_TYPE__ i32;
> +
> +void
> +clip (u16 *__restrict out, i32 *__restrict copy,
> +      const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      i32 clamped = x < 0 ? 0 : (x > 65535 ? 65535 : x);
> +      out[i] = clamped;
> +      copy[i] = clamped;
> +    }
> +}
> +
> +/* { dg-final { scan-tree-dump-not "sat_trunc pattern recognized" "vect" } } 
> */
> diff --git a/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-1.c 
> b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-1.c
> new file mode 100644
> index 00000000000..f159a8f4dac
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-1.c
> @@ -0,0 +1,7 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=armv8-a" } */
> +/* { dg-additional-options "-mmax-vectorization 
> --param=vect-epilogues-nomask=0 -fdump-tree-vect-details" } */
> +
> +#include "../../gcc.dg/vect/vect-sat-trunc-clamp-1.c"
> +
> +/* { dg-final { scan-tree-dump-times "sat_trunc pattern recognized" 4 "vect" 
> } } */
> diff --git 
> a/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-negative-1.c 
> b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-negative-1.c
> new file mode 100644
> index 00000000000..aa283f176ad
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/vect-sat-trunc-clamp-u-negative-1.c
> @@ -0,0 +1,30 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=armv8-a" } */
> +/* { dg-additional-options "-mmax-vectorization 
> --param=vect-epilogues-nomask=0 -fdump-tree-vect-details" } */
> +
> +typedef __UINT16_TYPE__ u16;
> +typedef __INT32_TYPE__ i32;
> +
> +__attribute__((noipa))
> +void
> +bad_low (u16 *__restrict out, const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      out[i] = x < 1 ? 1 : (x > 65535 ? 65535 : x);
> +    }
> +}
> +
> +__attribute__((noipa))
> +void
> +bad_high (u16 *__restrict out, const i32 *__restrict in, int n)
> +{
> +  for (int i = 0; i < n; ++i)
> +    {
> +      i32 x = in[i];
> +      out[i] = x > 65534 ? 65534 : (x < 0 ? 0 : x);
> +    }
> +}
> +
> +/* { dg-final { scan-tree-dump-not "sat_trunc pattern recognized" "vect" } } 
> */
> --
> 2.50.1 (Apple Git-155)
>

Reply via email to