From: Kyrylo Tkachov <[email protected]>
A double-word count of trailing or leading zeros written as
lo ? ctz (lo) : 32 + ctz (hi)
with lo and hi the low and high halves of X equals a single wide ctzll (X),
and likewise for clz, but GCC kept the branch and two narrow counts.
Recognize the idiom in phiopt diamond handling and emit one .CTZ/.CLZ on the
double-word value. As __builtin_c[lt]z is undefined at zero, so is the
result, so a one-argument .CTZ/.CLZ is used.
Based on a prototype by Andrea, refactored into helpers and extended
to handle clz as well as ctz, with testcases added.
Bootstrapped and tested on aarch64-none-linux-gnu and x86_64-linux.
PR tree-optimization/126035
gcc/ChangeLog:
* tree-ssa-phiopt.cc (clzctz_match_arm): New function.
(clzctz_decode_half): New function.
(clzctz_combine_replacement): New function.
(pass_phiopt::execute): Call it for the diamond case.
gcc/testsuite/ChangeLog:
* gcc.target/aarch64/pr126035.c: New test.
* gcc.target/aarch64/pr126035-clz.c: New test.
* gcc.target/aarch64/pr126035-run.c: New test.
* gcc.target/aarch64/pr126035-debug.c: New test.
* gcc.target/aarch64/pr126035-extra-pred.c: New test.
* gcc.target/aarch64/pr126035-narrow.c: New test.
Co-authored-by: Andrew Pinski <[email protected]>
Signed-off-by: Andrew Pinski <[email protected]>
Signed-off-by: Kyrylo Tkachov <[email protected]>
---
.../gcc.target/aarch64/pr126035-clz.c | 19 ++
.../gcc.target/aarch64/pr126035-debug.c | 19 ++
.../gcc.target/aarch64/pr126035-extra-pred.c | 35 +++
.../gcc.target/aarch64/pr126035-narrow.c | 33 +++
.../gcc.target/aarch64/pr126035-run.c | 50 ++++
gcc/testsuite/gcc.target/aarch64/pr126035.c | 26 ++
gcc/tree-ssa-phiopt.cc | 263 ++++++++++++++++++
7 files changed, 445 insertions(+)
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-clz.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-debug.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-extra-pred.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-narrow.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035-run.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr126035.c
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-clz.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-clz.c
new file mode 100644
index 00000000000..933668b6592
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-clz.c
@@ -0,0 +1,19 @@
+/* { dg-do compile } */
+/* { dg-options "-O2" } */
+
+#include <stdint.h>
+
+static inline unsigned clz32 (uint32_t v) { return (unsigned) __builtin_clz
(v); }
+
+/* The symmetric split CLZ32 idiom should be combined into a single double-word
+ clz, with no conditional select left over. */
+unsigned
+clz64_split (uint64_t val)
+{
+ uint32_t hi = (uint32_t) (val >> 32);
+ uint32_t lo = (uint32_t) val;
+ return hi ? clz32 (hi) : 32u + clz32 (lo);
+}
+
+/* { dg-final { scan-assembler-not {\tcsel\t} } } */
+/* { dg-final { scan-assembler-times {\tclz\t} 1 } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-debug.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-debug.c
new file mode 100644
index 00000000000..aae4c25c3ac
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-debug.c
@@ -0,0 +1,19 @@
+/* The combined count is inserted at the head of the merge block; with a named
+ intermediate a debug bind for it is present there under -g. Check we do not
+ ICE in verify_ssa. */
+/* { dg-do compile } */
+/* { dg-options "-O2 -g" } */
+
+#include <stdint.h>
+
+unsigned
+f (uint64_t val)
+{
+ uint32_t hi = (uint32_t) (val >> 32), lo = (uint32_t) val;
+ unsigned r;
+ if (lo)
+ r = (unsigned) __builtin_ctz (lo);
+ else
+ r = 32u + (unsigned) __builtin_ctz (hi);
+ return r;
+}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-extra-pred.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-extra-pred.c
new file mode 100644
index 00000000000..36db23aec81
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-extra-pred.c
@@ -0,0 +1,35 @@
+/* { dg-do run } */
+/* { dg-options "-O2" } */
+
+/* The merge block has a third incoming path; the split count must not be
+ combined and the value arriving on that path must be preserved. */
+
+unsigned __attribute__((noipa))
+f (unsigned long long x, int c, unsigned other)
+{
+ unsigned r;
+ unsigned lo = x, hi = x >> 32;
+ if (c)
+ {
+ r = other;
+ goto out;
+ }
+ if (lo)
+ r = __builtin_ctz (lo);
+ else
+ r = 32 + __builtin_ctz (hi);
+ out:
+ return r;
+}
+
+int
+main (void)
+{
+ if (f (4ULL, 1, 77) != 77)
+ __builtin_abort ();
+ if (f (4ULL, 0, 77) != 2)
+ __builtin_abort ();
+ if (f (1ULL << 52, 0, 77) != 52)
+ __builtin_abort ();
+ return 0;
+}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-narrow.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-narrow.c
new file mode 100644
index 00000000000..3258da2d0a5
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-narrow.c
@@ -0,0 +1,33 @@
+/* { dg-do run } */
+/* { dg-options "-O2" } */
+
+/* The high-half count is truncated by a narrow signed intermediate before
+ the word offset is added, so this is not the split double-word idiom and
+ must not be combined. */
+
+struct S { signed int c : 4; };
+
+unsigned __attribute__((noipa))
+f (unsigned long long x)
+{
+ struct S s;
+ unsigned lo = x, hi = x >> 32;
+ unsigned r;
+ if (lo)
+ r = __builtin_ctz (lo);
+ else
+ {
+ s.c = __builtin_ctz (hi);
+ r = s.c + 32;
+ }
+ return r;
+}
+
+int
+main (void)
+{
+ /* ctz (hi) == 20 truncates to 4 in the 4-bit field: 36, not 52. */
+ if (f (1ULL << 52) != 36)
+ __builtin_abort ();
+ return 0;
+}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr126035-run.c
b/gcc/testsuite/gcc.target/aarch64/pr126035-run.c
new file mode 100644
index 00000000000..0ff1a536313
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035-run.c
@@ -0,0 +1,50 @@
+/* { dg-do run } */
+/* { dg-require-effective-target ctz } */
+/* { dg-options "-O2" } */
+
+#include <stdint.h>
+
+static inline unsigned ctz32 (uint32_t v) { return (unsigned) __builtin_ctz
(v); }
+static inline unsigned clz32 (uint32_t v) { return (unsigned) __builtin_clz
(v); }
+
+unsigned __attribute__((noipa))
+ctz64_split (uint64_t val)
+{
+ uint32_t hi = (uint32_t) (val >> 32);
+ uint32_t lo = (uint32_t) val;
+ return lo ? ctz32 (lo) : 32u + ctz32 (hi);
+}
+
+unsigned __attribute__((noipa))
+clz64_split (uint64_t val)
+{
+ uint32_t hi = (uint32_t) (val >> 32);
+ uint32_t lo = (uint32_t) val;
+ return hi ? clz32 (hi) : 32u + clz32 (lo);
+}
+
+int
+main (void)
+{
+ uint64_t v = 0x9E3779B97F4A7C15ULL;
+ for (int i = 0; i < 200000; i++)
+ {
+ v = v * 6364136223846793005ULL + 1442695040888963407ULL;
+ uint64_t x = v;
+ switch (i & 7)
+ {
+ case 0: x = 1ULL << (v % 64); break;
+ case 1: x = (uint32_t) v; break;
+ case 2: x = ((uint64_t) (uint32_t) 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.c
b/gcc/testsuite/gcc.target/aarch64/pr126035.c
new file mode 100644
index 00000000000..33d481d3469
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr126035.c
@@ -0,0 +1,26 @@
+/* { dg-do compile } */
+/* { dg-options "-O2" } */
+
+#include <stdint.h>
+
+static inline unsigned ctz32 (uint32_t v) { return (unsigned) __builtin_ctz
(v); }
+
+unsigned
+ctz64_split (uint64_t val)
+{
+ uint32_t hi = (uint32_t) (val >> 32);
+ uint32_t lo = (uint32_t) val;
+ return lo ? ctz32 (lo) : 32u + ctz32 (hi);
+}
+
+const unsigned char *
+match_extend (const unsigned char *p, uint64_t x)
+{
+ return p + (ctz64_split (x) >> 3);
+}
+
+/* The split CTZ32 idiom should be combined into a single double-word ctz
+ (rbit + clz), with no conditional select left over. */
+/* { dg-final { scan-assembler-not {\tcsel\t} } } */
+/* { dg-final { scan-assembler-times {\trbit\t} 2 } } */
+/* { dg-final { scan-assembler-times {\tclz\t} 2 } } */
diff --git a/gcc/tree-ssa-phiopt.cc b/gcc/tree-ssa-phiopt.cc
index e12dc7a8b0c..faf954d2b42 100644
--- a/gcc/tree-ssa-phiopt.cc
+++ b/gcc/tree-ssa-phiopt.cc
@@ -2555,6 +2555,265 @@ 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. */
+
+static bool
+clzctz_match_arm (tree arg, combined_fn *pcfn, tree *phalf,
+ unsigned HOST_WIDE_INT *poffset)
+{
+ if (!INTEGRAL_TYPE_P (TREE_TYPE (arg)))
+ return false;
+
+ unsigned HOST_WIDE_INT offset = 0;
+ unsigned min_ebits = HOST_BITS_PER_WIDE_INT;
+ tree t = arg;
+
+ for (int limit = 0; limit < 8 && TREE_CODE (t) == SSA_NAME; limit++)
+ {
+ 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 (!is_gimple_assign (def))
+ break;
+ if (gimple_assign_cast_p (def)
+ && INTEGRAL_TYPE_P (TREE_TYPE (gimple_assign_rhs1 (def))))
+ t = gimple_assign_rhs1 (def);
+ else if (gimple_assign_rhs_code (def) == PLUS_EXPR
+ && tree_fits_uhwi_p (gimple_assign_rhs2 (def)))
+ {
+ offset += tree_to_uhwi (gimple_assign_rhs2 (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)
+ return false;
+ combined_fn cfn = gimple_call_combined_fn (call);
+ switch (cfn)
+ {
+ CASE_CFN_CLZ:
+ CASE_CFN_CTZ:
+ break;
+ default:
+ return false;
+ }
+
+ tree half = gimple_call_arg (call, 0);
+ if (TREE_CODE (half) != SSA_NAME)
+ return false;
+
+ /* Every type the count passes through must represent the count plus the
+ accumulated offset unchanged. A narrower signed intermediate (a signed
+ bit-field, say) is not canonicalized to a mask and would change the
+ value. */
+ unsigned prec = TYPE_PRECISION (TREE_TYPE (half));
+ if (min_ebits < HOST_BITS_PER_WIDE_INT
+ && offset + prec > (HOST_WIDE_INT_1U << min_ebits) - 1)
+ return false;
+
+ *pcfn = cfn;
+ *phalf = half;
+ *poffset = offset;
+ return true;
+}
+
+/* Helper for clzctz_combine_replacement. Decode HALF as the low or high half
+ of a double-word value: `(NARROW) X' (low half) or `(NARROW) (X >> W)' (high
+ half), where W == TYPE_PRECISION (NARROW) and X is an unsigned value of
+ precision 2 * W. On success return true and set *PBASE to X, *PWIDTH to W,
+ *PIS_HIGH to whether it is the high half and, if PINNER is non-null,
+ *PINNER to the value feeding the final truncation (X for the low half,
+ X >> W for the high half). */
+
+static bool
+clzctz_decode_half (tree half, tree *pbase, unsigned *pwidth, bool *pis_high,
+ tree *pinner)
+{
+ if (TREE_CODE (half) != SSA_NAME)
+ return false;
+ unsigned narrow_prec = TYPE_PRECISION (TREE_TYPE (half));
+ gimple *def = SSA_NAME_DEF_STMT (half);
+ if (!gimple_assign_cast_p (def))
+ return false;
+ tree inner = gimple_assign_rhs1 (def);
+ if (TREE_CODE (inner) != SSA_NAME
+ || !INTEGRAL_TYPE_P (TREE_TYPE (inner))
+ || !TYPE_UNSIGNED (TREE_TYPE (inner))
+ || TYPE_PRECISION (TREE_TYPE (inner)) != 2 * narrow_prec)
+ return false;
+
+ if (pinner)
+ *pinner = inner;
+ *pwidth = narrow_prec;
+
+ /* High half: inner == X >> W. */
+ gimple *idef = SSA_NAME_DEF_STMT (inner);
+ if (is_gimple_assign (idef)
+ && gimple_assign_rhs_code (idef) == RSHIFT_EXPR
+ && tree_fits_uhwi_p (gimple_assign_rhs2 (idef))
+ && tree_to_uhwi (gimple_assign_rhs2 (idef)) == narrow_prec)
+ {
+ *pbase = gimple_assign_rhs1 (idef);
+ *pis_high = true;
+ return true;
+ }
+
+ /* Otherwise the low half: inner == X. */
+ *pbase = inner;
+ *pis_high = false;
+ return true;
+}
+
+/* Attempt to combine a split clz/ctz idiom across a diamond into a single
+ double-word clz/ctz.
+
+ With LO = (NARROW) X and HI = (NARROW) (X >> W), W the precision of NARROW
+ and X a double-word value, the ctz form
+
+ if (LO != 0)
+ _a = (T) __builtin_ctz (LO);
+ else
+ _b = (T) (__builtin_ctz (HI) + W);
+ # res = PHI <_a, _b>
+
+ computes res = (T) ctzll (X), and the symmetric clz form
+
+ if (HI != 0)
+ _a = (T) __builtin_clz (HI);
+ else
+ _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.
+
+ 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
+ ARG0/ARG1 the corresponding PHI arguments. */
+
+static bool
+clzctz_combine_replacement (basic_block cond_bb, basic_block middle_bb,
+ basic_block middle_bb_alt, edge e0, edge e1,
+ gphi *phi, tree arg0, tree arg1)
+{
+ gcc_checking_assert (e0->dest == gimple_bb (phi)
+ && e1->dest == gimple_bb (phi)
+ && single_pred_p (middle_bb)
+ && single_pred_p (middle_bb_alt));
+
+ /* The merge block must have no other incoming edges: the PHI is replaced
+ by the combined count on all paths. */
+ if (EDGE_COUNT (gimple_bb (phi)->preds) != 2)
+ return false;
+
+ gcond *stmt = as_a <gcond *> (*gsi_last_bb (cond_bb));
+ tree_code cmp = gimple_cond_code (stmt);
+ tree cmp0 = gimple_cond_lhs (stmt);
+
+ if ((cmp != EQ_EXPR && cmp != NE_EXPR)
+ || !integer_zerop (gimple_cond_rhs (stmt))
+ || TREE_CODE (cmp0) != SSA_NAME
+ || !INTEGRAL_TYPE_P (TREE_TYPE (cmp0)))
+ return false;
+
+ if (TREE_CODE (arg0) != SSA_NAME || TREE_CODE (arg1) != SSA_NAME)
+ return false;
+
+ /* Find the arm reached when `cmp0 != 0' (the directly-counted half) and the
+ arm reached when `cmp0 == 0' (the other half plus the word offset). */
+ edge true_edge, false_edge;
+ extract_true_false_edges_from_block (cond_bb, &true_edge, &false_edge);
+ basic_block nz_bb = (cmp == NE_EXPR) ? true_edge->dest : false_edge->dest;
+ tree arg_nz = (nz_bb == middle_bb) ? arg0 : arg1;
+ tree arg_z = (nz_bb == middle_bb) ? arg1 : arg0;
+
+ 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)
+ || cfn_nz != cfn_z
+ || off_nz != 0)
+ return false;
+
+ bool is_clz;
+ switch (cfn_nz)
+ {
+ CASE_CFN_CTZ:
+ is_clz = false;
+ break;
+ CASE_CFN_CLZ:
+ is_clz = true;
+ break;
+ default:
+ return false;
+ }
+
+ tree base_nz, base_z, inner_nz;
+ unsigned w_nz, w_z;
+ bool high_nz, high_z;
+ if (!clzctz_decode_half (half_nz, &base_nz, &w_nz, &high_nz, &inner_nz)
+ || !clzctz_decode_half (half_z, &base_z, &w_z, &high_z, NULL)
+ || w_nz != w_z
+ || high_nz == high_z
+ || base_nz != base_z
+ || off_z != w_nz)
+ return false;
+
+ /* For ctz the directly-counted half is the low half; for clz it is the high
+ half. */
+ if (high_nz != is_clz)
+ return false;
+
+ /* The condition must test the nonzero-ness of the directly-counted half: the
+ half itself, or - for the high half - its pre-truncation value X >> W,
+ since (X >> W) != 0 iff (NARROW) (X >> W) != 0. */
+ if (cmp0 != half_nz
+ && !(high_nz && cmp0 == inner_nz))
+ return false;
+
+ tree fullvar = base_nz;
+ internal_fn ifn = is_clz ? IFN_CLZ : IFN_CTZ;
+ if (!direct_internal_fn_supported_p (ifn, TREE_TYPE (fullvar),
+ OPTIMIZE_FOR_BOTH))
+ return false;
+
+ 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));
+ gimple_stmt_iterator gsi = gsi_after_labels (gimple_bb (phi));
+ gsi_insert_before (&gsi, call, GSI_SAME_STMT);
+
+ auto_bitmap exprs_maybe_dce;
+ bitmap_set_bit (exprs_maybe_dce, SSA_NAME_VERSION (arg0));
+ bitmap_set_bit (exprs_maybe_dce, SSA_NAME_VERSION (arg1));
+ gsi = gsi_for_stmt (phi);
+ remove_phi_node (&gsi, false);
+ simple_dce_from_worklist (exprs_maybe_dce);
+
+ statistics_counter_event (cfun, "double-word clz/ctz combined", 1);
+ return true;
+}
+
/* Optimize x ? __builtin_fun (x) : C, where C is __builtin_fun (0).
Convert
@@ -4497,6 +4756,10 @@ pass_phiopt::execute (function *)
&& !diamond_p
&& spaceship_replacement (bb, bb1, e1, e2, phi, arg0, arg1))
cfgchanged = true;
+ else if (diamond_p
+ && clzctz_combine_replacement (bb, bb1, bb2, e1, e2, phi,
+ arg0, arg1))
+ cfgchanged = true;
};
execute_over_cond_phis (phiopt_exec);
--
2.50.1 (Apple Git-155)