Video codecs spell a clamp of a signed value to [0, M] as
(x & ~M) ? ((-x) >> (prec - 1)) & M : x, which the front end folds to
(unsigned) x > M ? ((-x) >> (prec - 1)) & M : x.  That is never turned
into MIN/MAX, so it if-converts into four vector operations where the
plain x < 0 ? 0 : x > M ? M : x spelling gives smax/smin.

The two forms agree except at x == INT_MIN, where -x is undefined, so
that arm is restricted to types with undefined signed overflow.

FFmpeg spells the same clamp with the complement instead of the negation,
(x & ~M) ? ((~x) >> (prec - 1)) & M : x (av_clip_uintp2, libavutil/common.h),
while x264 uses the negation (x264_clip_pixel, common/common.h).  ~x is
negative exactly when x is not, so it splats the same sign; it is total,
so that arm carries no overflow guard at all and stays correct under
-fwrapv and -ftrapv.  Both are now one pattern.

av_clip_uintp2 also returns unsigned rather than the type of x, so the
result and the unchanged arm can carry a conversion to the type the range
test already uses; the pattern accepts it and the guards read the type of x.
Without that the rule cannot fire on any of FFmpeg's nine, ten, twelve and
fourteen bit pixel writes, which is where the whole win is: aarch64 has no
assembly for those depths.  Recompiling FFmpeg's h264 and hevc DSP with and
without this patch, nothing else changed, .text falls by 6,892 bytes at -O2
and by 16,416 bytes at -O3, with sao_edge_restore_0_10 going from 3,252 to
1,588 bytes and pred8x8_plane_10_c from 1,308 to 512; h264qpel.c grows by
4,480 bytes at -O3, because a cheaper clamp changes what the vectorizer
decides to do around it.  Executed instructions for the ten-bit weighted
prediction and residual add, counted under qemu with one instruction per
translation block, fall from 10,464,997 to 6,444,997.

An exhaustive model of the guard at eight, ten and twelve bits, over every
threshold, every mask and every x, reports no unsound acceptance for either
spelling; the complement arm also has no sound pair that the guard rejects,
where the negation arm has one per width.

The threshold may be M or M - 1.  The masked arm is M for every x > 0,
so at x == M it is M, as the clamp is; (unsigned) x > M - 1 therefore
selects the same values as (unsigned) x > M.  M - 1 is how the
(unsigned) x >= M spelling arrives, since >= and < against a positive
constant are canonicalized to > and <= against the constant less one.
The <= spelling with the arms exchanged reaches this pattern as the >
form: fold_ternary_loc swaps the arms of a COND_EXPR on GENERIC and
phiopt retries every match with the inverted comparison on GIMPLE.

This is the neighbour of the conditional narrow clip added for PR127166:
that one narrows to a smaller type, this one clips at the width of x and
takes the mask the idiom applies to the shift.  Spelled on a sub-int
type the idiom does not match, as the negation is performed on the
promoted int, and a mask of 1 does not reach it either, since
((-x) >> (prec - 1)) & 1 is rewritten to (unsigned) x >> (prec - 1)
first.  Vectors do not reach the pattern: it matches a COND_EXPR
against INTEGER_CST thresholds with an integral result type, which a
vector spelling never is; for the negation the masked arm is in any
case rewritten to a compare mask by the (-x >> C) fold first.  The
pending "Saturating arithmetic matching improvements" series (Kyrylo
Tkachov, 2026-09-02) is disjoint: its clamp forms all narrow and end in
a .SAT_* call; this rule works at the width of x.

Bootstrapped and regression tested on x86_64-pc-linux-gnu.

Assisted-by: Claude Opus 5 (Anthropic)

gcc/ChangeLog:

        * match.pd ((unsigned) x > M ? ((-x) >> (prec - 1)) & M : x)
        ((unsigned) x > M ? ((~x) >> (prec - 1)) & M : x): New
        simplification for both spellings of the sign splat.

gcc/testsuite/ChangeLog:

        * gcc.dg/tree-ssa/clip-canon-1.c: New test.
        * gcc.dg/tree-ssa/clip-canon-2.c: New test.
        * gcc.dg/tree-ssa/clip-canon-3.c: New test.
        * gcc.dg/tree-ssa/clip-canon-4.c: New test.
        * gcc.dg/tree-ssa/clip-canon-5.c: New test.
        * gcc.dg/tree-ssa/clip-canon-6.c: New test.
        * gcc.dg/tree-ssa/clip-canon-7.c: New test.
        * gcc.dg/tree-ssa/clip-canon-8.c: New test.
        * gcc.dg/tree-ssa/clip-canon-9.c: New test.

Signed-off-by: Dominic P <[email protected]>
---
Andrea Pinski -- v6 extends the rule rather than changing what v5 did.

- Second spelling of the sign splat.  x264 writes the clamp with a
  negation, FFmpeg's av_clip_uintp2 with ~x.  ~x is negative exactly when
  x is not, and it is total, so that arm needs neither the
  undefined-overflow nor the no-sanitize guard and stays correct under
  -fwrapv and -ftrapv.  One (for splat (negate bit_not)) covers both.

- av_clip_uintp2 returns unsigned rather than the type of x, so the arms
  may carry a conversion to the type the range test already uses; v5
  could not fire there at all.  The guards now read the type of x, and a
  new !TYPE_UNSIGNED on it is what stops the complement arm matching an
  unsigned x, where the shift is logical (clip-canon-9.c).

- That is where the numbers are: those are the 9, 10, 12 and 14 bit pixel
  writes, and aarch64 has no assembly for them.  FFmpeg's h264 and hevc
  DSP loses 6,892 bytes of .text at -O2 and 16,416 at -O3 (h264qpel.c
  gains 4,480 at -O3, disclosed in the message); executed instructions
  for the ten-bit weighted prediction and residual add fall 38 %.

- I took your three functions as the gt, ge/lt and (x & ~255) != 0
  spellings, written out in clip-canon-5.c.  The two literal spellings
  stay in clip-canon-6.c as must-not-fold cases, since x & 255 and the
  reversed arms each compute a different function (x & 255 returns 256 at
  x == 256).  If you meant either literally, say so and I will follow.

- Still no separate le pattern: I could not make one fire, and a cc1 with
  one gives dumps identical to one without; say the word and I will write
  it out anyway.  The exhaustive model of the guard was re-run over both
  spellings: no unsound acceptance either way.

 gcc/match.pd                                 | 30 ++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c | 18 +++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c | 40 ++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c | 21 ++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c | 21 ++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c | 70 ++++++++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c | 33 +++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-7.c | 78 ++++++++++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-8.c | 34 +++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-9.c | 39 ++++++++++
 10 files changed, 384 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-7.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-8.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-9.c

diff --git a/gcc/match.pd b/gcc/match.pd
index 032f29dcbc2..d6cde161089 100644
--- a/gcc/match.pd
+++ b/gcc/match.pd
@@ -13390,6 +13390,36 @@ and,
                 (max @0 { build_zero_cst (TREE_TYPE (@0)); }))
                @1)))))))
 
+/* The clip of PR127166 at the width of X, with the mask spelled out and the
+   sign of X splat either way:
+
+     (UT) X > M ? ((-X) >> (PREC - 1)) & M : X  ->  MAX (MIN (X, M), 0)
+     (UT) X > M ? ((~X) >> (PREC - 1)) & M : X  ->  MAX (MIN (X, M), 0).
+
+   The mask is M at X == M, so a threshold of M - 1, which is how
+   (UT) X >= M is canonicalized, selects the same values.  ~X is negative
+   exactly when X is not, so it splats the same sign, and unlike -X it is
+   defined at the minimum of the type, so only the negation needs the
+   overflow guards.  The result may be converted to the unsigned type the
+   range test uses instead of keeping the type of X.  */
+(for splat (negate bit_not)
+ (simplify
+  (cond (gt (nop_convert@2 @0) INTEGER_CST@1)
+       (nop_convert? (bit_and:s (rshift (splat @0) INTEGER_CST@4)
+                                INTEGER_CST@5))
+       (nop_convert? @0))
+  (if (INTEGRAL_TYPE_P (type)
+       && !TYPE_UNSIGNED (TREE_TYPE (@0))
+       && TYPE_UNSIGNED (TREE_TYPE (@2))
+       && (splat == BIT_NOT_EXPR
+          || (TYPE_OVERFLOW_UNDEFINED (TREE_TYPE (@0))
+              && !TYPE_OVERFLOW_SANITIZED (TREE_TYPE (@0))))
+       && wi::to_wide (@4) == TYPE_PRECISION (TREE_TYPE (@0)) - 1
+       && tree_int_cst_sgn (@5) > 0
+       && (wi::to_wide (@1) == wi::to_wide (@5)
+          || wi::to_wide (@1) + 1 == wi::to_wide (@5)))
+   (convert (max (min @0 @5) { build_zero_cst (TREE_TYPE (@0)); })))))
+
 #if GIMPLE
 
 /* Include the saturation alu match patterns.  */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
new file mode 100644
index 00000000000..00edccdf7f3
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
@@ -0,0 +1,18 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-optimized" } */
+
+int
+clip_255 (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+int
+clip_1023 (int x)
+{
+  return (x & ~1023) ? ((-x) >> 31 & 1023) : x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 2 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 2 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
new file mode 100644
index 00000000000..233fde07267
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
@@ -0,0 +1,40 @@
+/* { dg-do run } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2" } */
+
+#define M 255
+
+/* x == INT_MIN makes -x undefined, so it is not compared here.  */
+__attribute__ ((noipa)) int
+idiom (int x)
+{
+  return (x & ~M) ? ((-x) >> 31 & M) : x;
+}
+
+/* The threshold M - 1 the (unsigned) x >= M spelling is folded to.  */
+__attribute__ ((noipa)) int
+idiom_ge (int x)
+{
+  return (unsigned) x >= M ? ((-x) >> 31 & M) : x;
+}
+
+__attribute__ ((noipa)) int
+clamp (int x)
+{
+  return x < 0 ? 0 : (x > M ? M : x);
+}
+
+int
+main (void)
+{
+  for (int i = -1000; i <= 1000; i++)
+    if (idiom (i) != clamp (i) || idiom_ge (i) != clamp (i))
+      __builtin_abort ();
+
+  int v[] = { __INT_MAX__, __INT_MAX__ - 1, -__INT_MAX__, -__INT_MAX__ + 1 };
+  for (unsigned i = 0; i < sizeof v / sizeof v[0]; i++)
+    if (idiom (v[i]) != clamp (v[i]) || idiom_ge (v[i]) != clamp (v[i]))
+      __builtin_abort ();
+
+  return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
new file mode 100644
index 00000000000..c1876653eaf
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
@@ -0,0 +1,21 @@
+/* { dg-do run } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fwrapv -fdump-tree-optimized" } */
+
+/* With wrapping overflow -INT_MIN is INT_MIN, so the idiom yields 255 at
+   INT_MIN where a clamp yields 0: the fold must not fire.  */
+__attribute__ ((noipa)) int
+idiom (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+int
+main (void)
+{
+  if (idiom (-__INT_MAX__ - 1) != 255)
+    __builtin_abort ();
+  return 0;
+}
+
+/* { dg-final { scan-tree-dump-not "MAX_EXPR |MIN_EXPR " "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c
new file mode 100644
index 00000000000..19116ed568f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c
@@ -0,0 +1,21 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fsanitize=signed-integer-overflow" } */
+/* { dg-additional-options "-fdump-tree-optimized" } */
+
+/* Keep the negation conditional so that it is checked at runtime.  */
+int
+clip_sanitized (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+/* An unsigned negation wraps, so this is not a clip.  */
+unsigned int
+clip_unsigned (unsigned int x)
+{
+  return (x & ~255u) ? ((-x) >> 31 & 255u) : x;
+}
+
+/* { dg-final { scan-tree-dump-not "MAX_EXPR |MIN_EXPR " "optimized" } } */
+/* { dg-final { scan-tree-dump {\.UBSAN_CHECK_SUB} "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
new file mode 100644
index 00000000000..3b9e76b0463
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
@@ -0,0 +1,70 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-phiopt1" } */
+
+/* The long form of the idiom: the mask is computed before the branch, so
+   the clip has to be recognized while the compare is still a branch.  */
+
+int
+clip_gt (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t > 255)
+    return tt;
+  return x;
+}
+
+int
+clip_ge (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t >= 255)
+    return tt;
+  return x;
+}
+
+int
+clip_le (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t <= 255)
+    return x;
+  return tt;
+}
+
+int
+clip_lt (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t < 255)
+    return x;
+  return tt;
+}
+
+int
+clip_bit_and (int x)
+{
+  int t = x & ~255;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 5 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 5 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-not "PHI <" "phiopt1" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c
new file mode 100644
index 00000000000..44dcd604f8c
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c
@@ -0,0 +1,33 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-phiopt1" } */
+
+/* X & 255 tests the bits the idiom keeps, not the ones it saturates on:
+   at X == 256 it is zero, so this returns 256 where a clip returns 255.  */
+int
+not_a_clip_bit_and (int x)
+{
+  int t = x & 255;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t)
+    return tt;
+  return x;
+}
+
+/* The arms the other way round: this returns 255 for X == 100.  */
+int
+not_a_clip_inverted (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t < 255)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-not "MIN_EXPR |MAX_EXPR " "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "PHI <" 2 "phiopt1" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-7.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-7.c
new file mode 100644
index 00000000000..27951337f77
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-7.c
@@ -0,0 +1,78 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-phiopt1 -fdump-tree-optimized" } */
+
+/* The second spelling of the sign splat, in the long form: the mask is
+   computed before the branch, so the clip has to be recognized while the
+   compare is still a branch.  This is av_clip_uintp2 in FFmpeg.  */
+
+int
+clip_not_gt (int x)
+{
+  unsigned t = x;
+  int tt = ~x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+int
+clip_not_ge (int x)
+{
+  unsigned t = x;
+  int tt = ~x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t >= 1024)
+    return tt;
+  return x;
+}
+
+/* A mask that is not 2**k - 1 clips just as well.  */
+int
+clip_not_odd (int x)
+{
+  unsigned t = x;
+  int tt = ~x;
+  tt = tt >> 31;
+  tt &= 1000;
+  if (t > 1000)
+    return tt;
+  return x;
+}
+
+/* The result type is the unsigned type of the range test rather than the
+   type of x, which is what av_clip_uintp2 returns.  It is not reformed into
+   a conditional expression until after phiopt1, hence the second dump.  */
+unsigned
+clip_not_unsigned_result (int x)
+{
+  unsigned t = x;
+  int tt = ~x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+/* The same, with the negation, to show the conversion is handled for both
+   spellings.  */
+unsigned
+clip_neg_unsigned_result (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 3 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 3 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 5 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 5 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-8.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-8.c
new file mode 100644
index 00000000000..14020457ed8
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-8.c
@@ -0,0 +1,34 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fwrapv -fdump-tree-optimized" } */
+
+/* With wrapping arithmetic -x is defined at the minimum and does not splat
+   the sign there, so the negation spelling must not be recognized.  ~x is
+   unaffected and still may be.  */
+
+int
+clip_neg (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+int
+clip_not (int x)
+{
+  unsigned t = x;
+  int tt = ~x;
+  tt = tt >> 31;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 1 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-9.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-9.c
new file mode 100644
index 00000000000..b1e601a0494
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-9.c
@@ -0,0 +1,39 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target lp64 } */
+/* { dg-options "-O2 -fdump-tree-optimized" } */
+
+/* The complement spelling reads the sign of ~x, so x has to be signed: on an
+   unsigned x the shift is logical and the mask is zero for every x, which is
+   not a clamp.  On LP64 unsigned long and unsigned long long are distinct
+   types of the same precision, so the compare carries a real conversion and
+   every structural test of the pattern is satisfied -- only the signedness of
+   x stops it.  */
+
+/* Signed x: the clamp.  */
+long
+clip_signed (long x)
+{
+  unsigned long long t = x;
+  long tt = ~x;
+  tt >>= 63;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+/* Unsigned x: must not be recognized.  */
+unsigned long
+clip_unsigned (unsigned long x)
+{
+  unsigned long long t = x;
+  unsigned long tt = ~x;
+  tt >>= 63;
+  tt &= 1023;
+  if (t > 1023)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 1 "optimized" } } */
-- 
2.55.0

Reply via email to