On 9/12/26 7:14 AM, Dominic P wrote:
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]>
I think this may have meaningful overlap with patches from Kyrylo in this space.  I think it would be worth trying your testcases against the trunk after Kyrylo's patches land to see if there were any meaningful gaps.

Jeff

Reply via email to