https://gcc.gnu.org/g:87c97883d659edc4b6d5cbece74d7ab7a3584a75
commit r17-4081-g87c97883d659edc4b6d5cbece74d7ab7a3584a75 Author: Kyrylo Tkachov <[email protected]> Date: Thu Jul 30 18:13:04 2026 +0200 match.pd: Narrow promoted saturating addition and subtraction Integer promotions and explicit vector conversions can leave a narrow saturating operation as extensions, a wide operation, a clamp, and a truncation. Simplify these forms: (type) MAX ((wide_type) A - (wide_type) B, 0) -> A - MIN (A, B) (type) MIN ((wide_type) A + (wide_type) B, MAX) -> A + MIN (~A, B) Require an intermediate type in which the operation cannot overflow. Subtraction needs a wider signed type. Addition needs one extra value bit and a further sign bit for a signed intermediate. Also require the exact maximum of the narrow type and one use of the outer clamp. For vector types, require target support for each narrow operation that the result introduces. The result exposes narrow saturating forms. AArch64 -O3, vector body: before: ldr q30, [x1, x4] ldr q29, [x2, x4] usubl v28.8h, v30.8b, v29.8b usubl2 v29.8h, v30.16b, v29.16b smax v28.8h, v28.8h, v31.8h smax v29.8h, v29.8h, v31.8h uzp1 v29.16b, v28.16b, v29.16b str q29, [x0, x4] after: ldr q31, [x1, x4] ldr q30, [x2, x4] uqsub v30.16b, v31.16b, v30.16b str q30, [x0, x4] The five computing instructions become one. Direct vector forms also remove the wide operations and conversions. Targets without a saturating instruction still use fewer operations. Scalar byte and halfword forms on AArch64 can cost one extra instruction because the backend uses SIMD registers. The direct narrow form has the same cost. Bootstrapped and tested on aarch64-none-linux-gnu. gcc/ChangeLog: * match.pd ((type) MAX ((wide_type) a - (wide_type) b, 0)): New simplification. ((type) MIN ((wide_type) a + (wide_type) b, MAX)): Likewise. gcc/testsuite/ChangeLog: * gcc.target/aarch64/sat_u_promoted-1.c: New test. * gcc.target/aarch64/sat_u_promoted-2.c: New test. * gcc.target/aarch64/sat_u_promoted-3.c: New test. * g++.target/aarch64/sat_u_promoted-vector-1.C: New test. Signed-off-by: Kyrylo Tkachov <[email protected]> Diff: --- gcc/match.pd | 46 ++++++++++++++++++++++ .../g++.target/aarch64/sat_u_promoted-vector-1.C | 35 ++++++++++++++++ .../gcc.target/aarch64/sat_u_promoted-1.c | 29 ++++++++++++++ .../gcc.target/aarch64/sat_u_promoted-2.c | 32 +++++++++++++++ .../gcc.target/aarch64/sat_u_promoted-3.c | 26 ++++++++++++ 5 files changed, 168 insertions(+) diff --git a/gcc/match.pd b/gcc/match.pd index 032f29dcbc25..8278cf1fd6db 100644 --- a/gcc/match.pd +++ b/gcc/match.pd @@ -4821,6 +4821,52 @@ DEFINE_INT_AND_FLOAT_ROUND_FN (RINT) @0 @2))) +/* Narrow a saturating add or subtract that promotions or explicit vector + conversions widened. + For an unsigned type, + (type) MAX ((wide_type) a - (wide_type) b, 0) -> a - MIN (a, b) + (type) MIN ((wide_type) a + (wide_type) b, MAX) -> a + MIN (~a, b) + Both results are exact in the narrow type: the difference is clamped at + zero and the sum at MAX, so the truncation never loses a bit. Require the + outer clamp to have one use. For a vector, also require target support for + each narrow operation that the result introduces. */ +(simplify + (convert (max:s (minus@2 (convert @0) (convert @1)) integer_zerop)) + (if (ANY_INTEGRAL_TYPE_P (type) + && TYPE_UNSIGNED (type) + && types_match (type, @0, @1) + && !TYPE_UNSIGNED (TREE_TYPE (@2)) + && element_precision (TREE_TYPE (@2)) > element_precision (type) + && (!VECTOR_TYPE_P (type) + || (target_supports_op_p (type, MIN_EXPR, optab_vector) + && target_supports_op_p (type, MINUS_EXPR, optab_vector)))) + (minus @0 (min @0 @1)))) + +(simplify + (convert (min:s (plus@2 (convert @0) (convert @1)) + uniform_integer_cst_p@3)) + (if (ANY_INTEGRAL_TYPE_P (type) + && TYPE_UNSIGNED (type) + && types_match (type, @0, @1) + && (!VECTOR_TYPE_P (type) + || (target_supports_op_p (type, BIT_NOT_EXPR, optab_vector) + && target_supports_op_p (type, MIN_EXPR, optab_vector) + && target_supports_op_p (type, PLUS_EXPR, optab_vector)))) + (with + { + unsigned precision = element_precision (type); + tree wide_type = TREE_TYPE (@2); + unsigned wide_precision = element_precision (wide_type); + tree max_cst = uniform_integer_cst_p (@3); + /* The widened sum reaches 2 * (2^precision - 1), which needs one bit + more than the narrow type, and one further bit for a sign. */ + unsigned needed = precision + (TYPE_UNSIGNED (wide_type) ? 1 : 2); + } + (if (wide_precision >= needed + && wi::eq_p (wi::to_wide (max_cst), + wi::mask (precision, false, wide_precision))) + (plus @0 (min @1 (bit_not @0))))))) + /* MIN (MAX (X, Y + CST), Y) -> Y for a positive CST, and the dual MAX (MIN (X, Y + CST), Y) -> Y for a negative one. The inner select is at least Y + CST, which the absence of wrapping puts strictly beyond Y, diff --git a/gcc/testsuite/g++.target/aarch64/sat_u_promoted-vector-1.C b/gcc/testsuite/g++.target/aarch64/sat_u_promoted-vector-1.C new file mode 100644 index 000000000000..b55f3667426e --- /dev/null +++ b/gcc/testsuite/g++.target/aarch64/sat_u_promoted-vector-1.C @@ -0,0 +1,35 @@ +/* { dg-do compile } */ +/* { dg-options "-O2 -fdump-tree-optimized" } */ + +typedef unsigned char u8x8 __attribute__((vector_size(8))); +typedef short i16x8 __attribute__((vector_size(16))); + +u8x8 +subv (u8x8 a, u8x8 b) +{ + i16x8 x = __builtin_convertvector (a, i16x8); + i16x8 y = __builtin_convertvector (b, i16x8); + i16x8 d = x - y; + i16x8 zero = { 0, 0, 0, 0, 0, 0, 0, 0 }; + i16x8 r = d < zero ? zero : d; + return __builtin_convertvector (r, u8x8); +} + +u8x8 +addv (u8x8 a, u8x8 b) +{ + i16x8 x = __builtin_convertvector (a, i16x8); + i16x8 y = __builtin_convertvector (b, i16x8); + i16x8 s = x + y; + i16x8 max = { 255, 255, 255, 255, 255, 255, 255, 255 }; + i16x8 r = s > max ? max : s; + return __builtin_convertvector (r, u8x8); +} + +/* { dg-final { scan-tree-dump-times "MIN_EXPR" 2 "optimized" } } */ +/* { dg-final { scan-tree-dump-not "MAX_EXPR" "optimized" } } */ +/* { dg-final { scan-tree-dump-not "i16x8" "optimized" } } */ +/* { dg-final { scan-assembler-times {\tumin\tv[0-9]+\.8b} 2 } } */ +/* { dg-final { scan-assembler-times {\tsub\tv[0-9]+\.8b} 1 } } */ +/* { dg-final { scan-assembler-times {\tnot\tv[0-9]+\.8b} 1 } } */ +/* { dg-final { scan-assembler-times {\tadd\tv[0-9]+\.8b} 1 } } */ diff --git a/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-1.c b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-1.c new file mode 100644 index 000000000000..424d2df3fba3 --- /dev/null +++ b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-1.c @@ -0,0 +1,29 @@ +/* { dg-do compile } */ +/* { dg-options "-O2 -fdump-tree-optimized" } */ + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; + +/* The integer promotions turn a narrow saturating add or subtract written + as a clamp into a widened operation feeding a MIN or a MAX. */ + +u8 sub8 (u8 a, u8 b) { int t = a - b; return t < 0 ? 0 : t; } +u8 sub8_swapped (u8 a, u8 b) { int t = a - b; return t > 0 ? t : 0; } +u16 sub16 (u16 a, u16 b) { int t = a - b; return t < 0 ? 0 : t; } +u32 sub32 (u32 a, u32 b) { long long t = (long long) a - b; return t < 0 ? 0 : t; } + +u8 add8 (u8 a, u8 b) { int t = a + b; return t > 255 ? 255 : t; } +u16 add16 (u16 a, u16 b) { int t = a + b; return t > 65535 ? 65535 : t; } +u32 add32 (u32 a, u32 b) { u64 t = (u64) a + b; return t > 0xffffffffu ? 0xffffffffu : t; } + +u8 add8_shared (u8 a, u8 b, int *p) +{ + int t = a + b; + *p = t; + return t > 255 ? 255 : t; +} + +/* { dg-final { scan-tree-dump-times "\\.SAT_SUB " 4 "optimized" } } */ +/* { dg-final { scan-tree-dump-times "\\.SAT_ADD " 4 "optimized" } } */ diff --git a/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-2.c b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-2.c new file mode 100644 index 000000000000..0e7a3c701c9e --- /dev/null +++ b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-2.c @@ -0,0 +1,32 @@ +/* { dg-do compile } */ +/* { dg-options "-O3 -fdump-tree-optimized" } */ + +typedef unsigned char u8; + +/* These loops expose narrow saturating operations that vectorize to one + uqsub or uqadd. */ + +void +sub_loop (u8 *__restrict d, u8 *__restrict a, u8 *__restrict b, int n) +{ + for (int i = 0; i < n; i++) + { + int t = a[i] - b[i]; + d[i] = t < 0 ? 0 : t; + } +} + +void +add_loop (u8 *__restrict d, u8 *__restrict a, u8 *__restrict b, int n) +{ + for (int i = 0; i < n; i++) + { + int t = a[i] + b[i]; + d[i] = t > 255 ? 255 : t; + } +} + +/* { dg-final { scan-tree-dump "\\.SAT_SUB " "optimized" } } */ +/* { dg-final { scan-tree-dump "\\.SAT_ADD " "optimized" } } */ +/* { dg-final { scan-assembler "uqsub\tv\[0-9\]+\.16b" } } */ +/* { dg-final { scan-assembler "uqadd\tv\[0-9\]+\.16b" } } */ diff --git a/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-3.c b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-3.c new file mode 100644 index 000000000000..4ec4abc36bcd --- /dev/null +++ b/gcc/testsuite/gcc.target/aarch64/sat_u_promoted-3.c @@ -0,0 +1,26 @@ +/* { dg-do compile } */ +/* { dg-options "-O2 -fdump-tree-optimized" } */ + +typedef unsigned char u8; + +/* Keep the outer clamp live so that the narrowing rules do not fire. */ + +u8 +sub8_result_shared (u8 a, u8 b, int *out) +{ + int t = a - b; + int r = t < 0 ? 0 : t; + *out = r; + return r; +} + +u8 +add8_result_shared (u8 a, u8 b, int *out) +{ + int t = a + b; + int r = t > 255 ? 255 : t; + *out = r; + return r; +} + +/* { dg-final { scan-tree-dump-not "\\.SAT_" "optimized" } } */
