From: Kyrylo Tkachov <[email protected]>
Extend the double-word clz/ctz combine to the software De Bruijn ctz/clz
fallbacks (as used by zstd when hardware count instructions are disabled).
GCC recognizes each 32-bit fallback as a two-argument .C[LT]Z (HALF, W)
defined at zero, optionally followed by an "AND (W - 1)" mask or a
"HALF == 0 ? C : ..." select that sets the value at zero. Match those
per-half forms, compute the combined value at zero (the word offset plus the
offset arm value there) and emit a single wide count, preserving that value
with a two-argument .C[LT]Z when the target already produces it, or an
explicit "X == 0 ? value : count" otherwise.
Bootstrapped and tested on aarch64-none-linux-gnu and x86_64-linux.
This patch gives ~3% on the zstd benchmark in SPEC2026 on my aarch64
system.
PR tree-optimization/126035
gcc/ChangeLog:
* tree-ssa-phiopt.cc (clzctz_match_arm): Handle a two-argument
.C[LT]Z and an optional zero-only mask or select.
(clzctz_combine_replacement): Preserve a defined value at zero.
gcc/testsuite/ChangeLog:
* gcc.target/aarch64/pr126035-debruijn.c: New test.
* gcc.target/aarch64/pr126035-debruijn-run.c: New test.
* gcc.target/aarch64/pr126035-debruijn-g.c: New test.
Signed-off-by: Kyrylo Tkachov <[email protected]>
---
.../gcc.target/aarch64/pr126035-debruijn-g.c | 30 +++
.../aarch64/pr126035-debruijn-run.c | 78 +++++++
.../gcc.target/aarch64/pr126035-debruijn.c | 50 ++++
gcc/tree-ssa-phiopt.cc | 220 ++++++++++++++++--
4 files changed, 358 insertions(+), 20 deletions(-)
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-g.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-run.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-debruijn.c
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-g.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-g.c
new file mode 100644
index 00000000000..539fdf48275
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-g.c
@@ -0,0 +1,30 @@
+/* Like the plain-builtin -g test but for the De Bruijn (defined-at-zero)
+ select-fixup path of the combine; check we do not ICE in verify_ssa under
+ -g. */
+/* { dg-do compile } */
+/* { dg-options "-O2 -g" } */
+
+#include <stdint.h>
+typedef uint32_t U32;
+typedef uint64_t U64;
+
+static inline unsigned
+ctz32_fallback (U32 val)
+{
+ static const U32 T[32] = {
+ 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
+ 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 };
+ return T[((U32) ((val & -(U32) val) * 0x077CB531U)) >> 27];
+}
+
+unsigned
+f (U64 val)
+{
+ U32 hi = (U32) (val >> 32), lo = (U32) val;
+ unsigned r;
+ if (lo == 0)
+ r = 32 + ctz32_fallback (hi);
+ else
+ r = ctz32_fallback (lo);
+ return r;
+}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-run.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-run.c
new file mode 100644
index 00000000000..90353537a4a
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn-run.c
@@ -0,0 +1,78 @@
+/* { dg-do run } */
+/* { dg-options "-O2" } */
+
+/* Run-time check of the De Bruijn split clz/ctz combine, including the
+ values the fallbacks define at zero (ctz: 32, clz: 63), which the combine
+ must preserve. */
+
+#include <stdint.h>
+typedef uint32_t U32;
+typedef uint64_t U64;
+
+static inline unsigned
+ctz32_fallback (U32 val)
+{
+ static const U32 T[32] = {
+ 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
+ 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 };
+ return T[((U32) ((val & -(U32) val) * 0x077CB531U)) >> 27];
+}
+
+static inline unsigned
+clz32_fallback (U32 val)
+{
+ static const U32 T[32] = {
+ 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
+ 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 };
+ val |= val >> 1; val |= val >> 2; val |= val >> 4;
+ val |= val >> 8; val |= val >> 16;
+ return 31 - T[(val * 0x07C4ACDDU) >> 27];
+}
+
+unsigned __attribute__((noipa))
+ctz64_split (U64 val)
+{
+ U32 msw = (U32) (val >> 32);
+ U32 lsw = (U32) val;
+ return lsw == 0 ? 32 + ctz32_fallback (msw) : ctz32_fallback (lsw);
+}
+
+unsigned __attribute__((noipa))
+clz64_split (U64 val)
+{
+ U32 msw = (U32) (val >> 32);
+ U32 lsw = (U32) val;
+ return msw == 0 ? 32 + clz32_fallback (lsw) : clz32_fallback (msw);
+}
+
+int
+main (void)
+{
+ /* Values defined by the fallbacks at zero must be preserved. */
+ if (ctz64_split (0) != 32)
+ __builtin_abort ();
+ if (clz64_split (0) != 63)
+ __builtin_abort ();
+
+ U64 v = 0x9E3779B97F4A7C15ULL;
+ for (int i = 0; i < 200000; i++)
+ {
+ v = v * 6364136223846793005ULL + 1442695040888963407ULL;
+ U64 x = v;
+ switch (i & 7)
+ {
+ case 0: x = 1ULL << (v % 64); break;
+ case 1: x = (U32) v; break;
+ case 2: x = ((U64) (U32) v) << 32; break;
+ case 3: x = ~0ULL; break;
+ default: break;
+ }
+ if (x == 0)
+ x = 1;
+ if (ctz64_split (x) != (unsigned) __builtin_ctzll (x))
+ __builtin_abort ();
+ if (clz64_split (x) != (unsigned) __builtin_clzll (x))
+ __builtin_abort ();
+ }
+ return 0;
+}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn.c
new file mode 100644
index 00000000000..64277a24758
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-debruijn.c
@@ -0,0 +1,50 @@
+/* { dg-do compile } */
+/* { dg-options "-O2 -fdump-tree-optimized" } */
+
+/* The De Bruijn software ctz/clz fallbacks are recognized as defined-at-zero
+ .CTZ/.CLZ. The split double-word forms should still be combined into a
+ single wide .CTZ/.CLZ (with the defined value at zero preserved by a
+ select). */
+
+#include <stdint.h>
+typedef uint32_t U32;
+typedef uint64_t U64;
+
+static inline unsigned
+ctz32_fallback (U32 val)
+{
+ static const U32 T[32] = {
+ 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
+ 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 };
+ return T[((U32) ((val & -(U32) val) * 0x077CB531U)) >> 27];
+}
+
+static inline unsigned
+clz32_fallback (U32 val)
+{
+ static const U32 T[32] = {
+ 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
+ 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 };
+ val |= val >> 1; val |= val >> 2; val |= val >> 4;
+ val |= val >> 8; val |= val >> 16;
+ return 31 - T[(val * 0x07C4ACDDU) >> 27];
+}
+
+unsigned
+ctz64_split (U64 val)
+{
+ U32 msw = (U32) (val >> 32);
+ U32 lsw = (U32) val;
+ return lsw == 0 ? 32 + ctz32_fallback (msw) : ctz32_fallback (lsw);
+}
+
+unsigned
+clz64_split (U64 val)
+{
+ U32 msw = (U32) (val >> 32);
+ U32 lsw = (U32) val;
+ return msw == 0 ? 32 + clz32_fallback (lsw) : clz32_fallback (msw);
+}
+
+/* { dg-final { scan-tree-dump-times {= \.CTZ } 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times {= \.CLZ } 1 "optimized" } } */
diff --git a/gcc/tree-ssa-phiopt.cc b/gcc/tree-ssa-phiopt.cc
index faf954d2b42..0ae525bd070 100644
--- a/gcc/tree-ssa-phiopt.cc
+++ b/gcc/tree-ssa-phiopt.cc
@@ -2555,16 +2555,23 @@ spaceship_replacement (basic_block cond_bb, basic_block
middle_bb,
return true;
}
-/* Helper for clzctz_combine_replacement. Match ARG as
- (T) __builtin_c[lt]z (HALF)
- with an optional integer constant added (the high-half word offset) and
- integer conversions wrapped around it. On success return true and set *PCFN
- to the combined_fn of the (single-argument) clz/ctz call, *PHALF to its
- argument and *POFFSET to the added constant. */
+/* Helper for clzctz_combine_replacement. Match ARG as a per-half count
+
+ (T) [ __builtin_c[lt]z (HALF) | .C[LT]Z (HALF[, Z]) ]
+
+ with an optional integer constant added (the high-half word offset), an
+ optional zero-only correction (a `& (W - 1)' mask or a
+ `HALF == 0 ? C : count' select, as emitted for the De Bruijn software
+ fallbacks) and integer conversions wrapped around it. On success return
+ true and set *PCFN to the call's combined_fn, *PHALF to its argument and
+ *POFFSET to the added constant. If PZERO_KNOWN/PZERO_VAL are non-null they
+ receive whether the count's value at HALF == 0 is known and, if so, that
+ value (undefined for the plain one-argument builtins). */
static bool
clzctz_match_arm (tree arg, combined_fn *pcfn, tree *phalf,
- unsigned HOST_WIDE_INT *poffset)
+ unsigned HOST_WIDE_INT *poffset, bool *pzero_known,
+ unsigned HOST_WIDE_INT *pzero_val)
{
if (!INTEGRAL_TYPE_P (TREE_TYPE (arg)))
return false;
@@ -2573,6 +2580,7 @@ clzctz_match_arm (tree arg, combined_fn *pcfn, tree
*phalf,
unsigned min_ebits = HOST_BITS_PER_WIDE_INT;
tree t = arg;
+ /* Peel integer conversions and the constant word offset. */
for (int limit = 0; limit < 8 && TREE_CODE (t) == SSA_NAME; limit++)
{
unsigned ebits
@@ -2594,10 +2602,70 @@ clzctz_match_arm (tree arg, combined_fn *pcfn, tree
*phalf,
break;
}
+ /* Peel an optional zero-only correction: a `& M' mask or a
+ `HALF == 0 ? C : count' select. */
+ bool have_mask = false, have_select = false;
+ unsigned HOST_WIDE_INT mask = 0;
+ tree sel_cond = NULL_TREE, sel_const = NULL_TREE;
+ bool sel_true_is_const = false;
+ if (TREE_CODE (t) == SSA_NAME)
+ {
+ gimple *def = SSA_NAME_DEF_STMT (t);
+ if (is_gimple_assign (def)
+ && gimple_assign_rhs_code (def) == BIT_AND_EXPR
+ && tree_fits_uhwi_p (gimple_assign_rhs2 (def)))
+ {
+ have_mask = true;
+ mask = tree_to_uhwi (gimple_assign_rhs2 (def));
+ t = gimple_assign_rhs1 (def);
+ }
+ else if (is_gimple_assign (def)
+ && gimple_assign_rhs_code (def) == COND_EXPR)
+ {
+ tree tv = gimple_assign_rhs2 (def);
+ tree fv = gimple_assign_rhs3 (def);
+ if (TREE_CODE (tv) == INTEGER_CST && TREE_CODE (fv) == SSA_NAME)
+ {
+ sel_const = tv;
+ sel_true_is_const = true;
+ t = fv;
+ }
+ else if (TREE_CODE (fv) == INTEGER_CST && TREE_CODE (tv) == SSA_NAME)
+ {
+ sel_const = fv;
+ sel_true_is_const = false;
+ t = tv;
+ }
+ else
+ return false;
+ if (!tree_fits_uhwi_p (sel_const))
+ return false;
+ have_select = true;
+ sel_cond = gimple_assign_rhs1 (def);
+ }
+ }
+
+ /* Peel conversions before the call. */
+ while (TREE_CODE (t) == SSA_NAME)
+ {
+ unsigned ebits
+ = TYPE_PRECISION (TREE_TYPE (t)) - !TYPE_UNSIGNED (TREE_TYPE (t));
+ min_ebits = MIN (min_ebits, ebits);
+ gimple *def = SSA_NAME_DEF_STMT (t);
+ if (gimple_assign_cast_p (def)
+ && INTEGRAL_TYPE_P (TREE_TYPE (gimple_assign_rhs1 (def))))
+ t = gimple_assign_rhs1 (def);
+ else
+ break;
+ }
+
if (TREE_CODE (t) != SSA_NAME)
return false;
gimple *call = SSA_NAME_DEF_STMT (t);
- if (!is_gimple_call (call) || gimple_call_num_args (call) != 1)
+ if (!is_gimple_call (call))
+ return false;
+ unsigned nargs = gimple_call_num_args (call);
+ if (nargs != 1 && nargs != 2)
return false;
combined_fn cfn = gimple_call_combined_fn (call);
switch (cfn)
@@ -2622,9 +2690,71 @@ clzctz_match_arm (tree arg, combined_fn *pcfn, tree
*phalf,
&& offset + prec > (HOST_WIDE_INT_1U << min_ebits) - 1)
return false;
+ /* Value of the bare call at HALF == 0: the explicit operand of a
+ two-argument .C[LT]Z, otherwise undefined. */
+ bool zero_known = false;
+ unsigned HOST_WIDE_INT zero_val = 0;
+ if (nargs == 2)
+ {
+ tree z = gimple_call_arg (call, 1);
+ if (TREE_CODE (z) != INTEGER_CST || !tree_fits_uhwi_p (z))
+ return false;
+ zero_known = true;
+ zero_val = tree_to_uhwi (z);
+ }
+
+ if (have_mask)
+ {
+ /* The mask must leave any valid count in [0, prec - 1] unchanged. */
+ if ((mask & (prec - 1)) != prec - 1)
+ return false;
+ zero_val &= mask;
+ }
+ else if (have_select)
+ {
+ /* The select must read `HALF == 0 ? C : count', overriding only the
+ value at zero. */
+ tree_code rel;
+ tree l, r;
+ if (TREE_CODE (sel_cond) == SSA_NAME
+ && is_gimple_assign (SSA_NAME_DEF_STMT (sel_cond))
+ && (TREE_CODE_CLASS (gimple_assign_rhs_code
+ (SSA_NAME_DEF_STMT (sel_cond)))
+ == tcc_comparison))
+ {
+ gimple *cdef = SSA_NAME_DEF_STMT (sel_cond);
+ rel = gimple_assign_rhs_code (cdef);
+ l = gimple_assign_rhs1 (cdef);
+ r = gimple_assign_rhs2 (cdef);
+ }
+ else if (COMPARISON_CLASS_P (sel_cond))
+ {
+ rel = TREE_CODE (sel_cond);
+ l = TREE_OPERAND (sel_cond, 0);
+ r = TREE_OPERAND (sel_cond, 1);
+ }
+ else
+ return false;
+ if ((rel != EQ_EXPR && rel != NE_EXPR)
+ || l != half
+ || !integer_zerop (r))
+ return false;
+ /* The value at HALF == 0 must be the constant, not the count. */
+ bool picks_const_at_zero
+ = (rel == EQ_EXPR) ? sel_true_is_const : !sel_true_is_const;
+ if (!picks_const_at_zero)
+ return false;
+ zero_known = true;
+ zero_val = tree_to_uhwi (sel_const);
+ }
+
*pcfn = cfn;
*phalf = half;
*poffset = offset;
+ if (pzero_known)
+ *pzero_known = zero_known;
+ if (pzero_val)
+ *pzero_val = zero_val;
return true;
}
@@ -2695,9 +2825,14 @@ clzctz_decode_half (tree half, tree *pbase, unsigned
*pwidth, bool *pis_high,
_b = (T) (__builtin_clz (LO) + W);
# res = PHI <_a, _b>
- computes res = (T) clzll (X). As the original __builtin_c[lt]z calls are
- undefined at zero, X == 0 is undefined in both the idiom and the result, so
- the combined count is emitted as a one-argument .CTZ/.CLZ.
+ computes res = (T) clzll (X). The per-half counts are either plain
+ __builtin_c[lt]z (undefined at zero) or, for the De Bruijn software
+ fallbacks GCC recognizes, a `.C[LT]Z (HALF, W)' optionally wrapped in a
+ `& (W - 1)' mask or `HALF == 0 ? C : ...' select that defines the value at
+ zero. When the result is undefined at X == 0 a one-argument .C[LT]Z is
+ emitted; when it is defined, the value at zero is reproduced either by a
+ two-argument .C[LT]Z (if the target already yields it) or by an explicit
+ `X == 0 ? value : count'.
COND_BB ends in `cmp0 [!=|==] 0'; MIDDLE_BB and MIDDLE_BB_ALT are the two
arms, E0/E1 the edges from them into the merge block holding PHI, and
@@ -2742,8 +2877,10 @@ clzctz_combine_replacement (basic_block cond_bb,
basic_block middle_bb,
combined_fn cfn_nz, cfn_z;
tree half_nz, half_z;
unsigned HOST_WIDE_INT off_nz, off_z;
- if (!clzctz_match_arm (arg_nz, &cfn_nz, &half_nz, &off_nz)
- || !clzctz_match_arm (arg_z, &cfn_z, &half_z, &off_z)
+ bool zk_z;
+ unsigned HOST_WIDE_INT zv_z;
+ if (!clzctz_match_arm (arg_nz, &cfn_nz, &half_nz, &off_nz, NULL, NULL)
+ || !clzctz_match_arm (arg_z, &cfn_z, &half_z, &off_z, &zk_z, &zv_z)
|| cfn_nz != cfn_z
|| off_nz != 0)
return false;
@@ -2790,18 +2927,61 @@ clzctz_combine_replacement (basic_block cond_bb,
basic_block middle_bb,
OPTIMIZE_FOR_BOTH))
return false;
+ /* When the counts are defined at zero (the De Bruijn fallbacks) the
+ combined count must keep the idiom's value at X == 0, OFF_Z + ZV_Z: with
+ a two-argument .C[LT]Z when the target already yields it, or an explicit
+ "X == 0 ? value : count" otherwise. */
+ unsigned HOST_WIDE_INT zero_at = 0;
+ bool zero_matches_hw = false;
+ if (zk_z)
+ {
+ zero_at = off_z + zv_z;
+ if (zero_at > TYPE_PRECISION (TREE_TYPE (fullvar)))
+ return false;
+ int hwz = 0;
+ scalar_int_mode mode = SCALAR_INT_TYPE_MODE (TREE_TYPE (fullvar));
+ int defined = (is_clz
+ ? CLZ_DEFINED_VALUE_AT_ZERO (mode, hwz)
+ : CTZ_DEFINED_VALUE_AT_ZERO (mode, hwz));
+ zero_matches_hw = (defined == 2
+ && (unsigned HOST_WIDE_INT) hwz == zero_at);
+ }
+
if (dump_file && (dump_flags & TDF_DETAILS))
fprintf (dump_file, "Combining double-word %s in block %d.\n",
is_clz ? "clz" : "ctz", cond_bb->index);
- /* Replace the PHI with a single wide clz/ctz of the full value defining the
- PHI result. As __builtin_c[lt]z is undefined at zero, a one-argument
- .C[LT]Z is used. Insert it at the head of the merge block, before any
- leading debug bind of the PHI result. */
- gcall *call = gimple_build_call_internal (ifn, 1, fullvar);
- gimple_call_set_lhs (call, gimple_phi_result (phi));
+ /* Build the wide count to replace the PHI, defining the PHI result.
+ Insert it at the head of the merge block, before any leading debug bind
+ of that result. */
+ tree res = gimple_phi_result (phi);
+ gimple_seq seq = NULL;
+ gimple *def;
+ if (!zk_z || zero_matches_hw)
+ {
+ gcall *call
+ = zk_z
+ ? gimple_build_call_internal (ifn, 2, fullvar,
+ build_int_cst (integer_type_node,
+ zero_at))
+ : gimple_build_call_internal (ifn, 1, fullvar);
+ gimple_call_set_lhs (call, res);
+ def = call;
+ }
+ else
+ {
+ /* res = (fullvar == 0) ? zero_at : .C[LT]Z (fullvar). */
+ tree cnt = gimple_build (&seq, as_combined_fn (ifn), TREE_TYPE (res),
+ fullvar);
+ tree zerop = gimple_build (&seq, EQ_EXPR, boolean_type_node, fullvar,
+ build_zero_cst (TREE_TYPE (fullvar)));
+ def = gimple_build_assign (res, COND_EXPR, zerop,
+ build_int_cst (TREE_TYPE (res), zero_at),
+ cnt);
+ }
gimple_stmt_iterator gsi = gsi_after_labels (gimple_bb (phi));
- gsi_insert_before (&gsi, call, GSI_SAME_STMT);
+ gsi_insert_seq_before (&gsi, seq, GSI_SAME_STMT);
+ gsi_insert_before (&gsi, def, GSI_SAME_STMT);
auto_bitmap exprs_maybe_dce;
bitmap_set_bit (exprs_maybe_dce, SSA_NAME_VERSION (arg0));
--
2.50.1 (Apple Git-155)