On Fri, Sep 4, 2026 at 5:38 AM <[email protected]> wrote:
>
> From: Kyrylo Tkachov <[email protected]>
>
> Reassociation rebalances a chain of four or more comparisons into a tree.
> The reassociation width of the boolean mode is the width of an integer
> bitwise operation. A conditional-compare sequence cannot use that
> parallelism because every conditional comparison tests the flags that the
> previous comparison set. For
>
> int
> f (int a, int b, int c, int d, int e, int f, int g, int h)
> {
> return (a < b) & (c < d) & (e < f) & (g < h);
> }
>
> reassociation produced:
>
> _3 = _1 & _2;
> _16 = _4 & _5;
> _17 = _3 & _16;
>
> AArch64 emitted two sequences and a bitwise operation:
>
> cmp w0, w1
> ccmp w2, w3, 0, lt
> cset w1, lt
> cmp w4, w5
> ccmp w6, w7, 0, lt
> cset w0, lt
> and w0, w1, w0
>
> Keep the chain linear only when every operand is a scalar comparison in the
> same block and at least one comparison can be folded into the flags. Put a
> forwarded trapping comparison in the innermost pair. The expander tries
> both orders of that pair and can make this comparison unconditional.
> AArch64 then emits one sequence:
>
> cmp w4, w5
> ccmp w6, w7, 0, lt
> ccmp w2, w3, 0, lt
> ccmp w0, w1, 0, lt
> cset w0, lt
>
> Tests cover linear and balanced source forms, unsupported operands,
> trapping comparisons in every source position, and comparison results with
> other uses.
>
> Changes since v1:
> - Require the complete operand list to form one CCMP sequence.
> - Put a trapping comparison first and reject conflicting heads.
> - Add tests for trapping positions and comparison results with other uses.
>
> gcc/
>
> * tree-ssa-reassoc.cc (ccmp_comparison_p): New function.
> (prepare_ccmp_chain): Likewise.
> (reassociate_bb): Keep a complete conditional-compare chain linear
> and put its required first operand in the innermost pair.
>
> gcc/testsuite/
>
> * gcc.c-torture/execute/ccmp-tree-1.c: New test.
> * gcc.target/aarch64/ccmp_6.c: New test.
> * gcc.target/aarch64/ccmp_10.c: Likewise.
> * gcc.target/aarch64/ccmp_12.c: Likewise.
> * gcc.target/aarch64/ccmp_14.c: Likewise.
> * gcc.target/aarch64/fccmp_6.c: Likewise.
> * gcc.target/aarch64/fccmp_8.c: Likewise.
>
> Signed-off-by: Kyrylo Tkachov <[email protected]>
>
> ---
> diff --git a/gcc/testsuite/gcc.c-torture/execute/ccmp-tree-1.c
> b/gcc/testsuite/gcc.c-torture/execute/ccmp-tree-1.c
> new file mode 100644
> index 00000000000..9d7f5241f19
> --- /dev/null
> +++ b/gcc/testsuite/gcc.c-torture/execute/ccmp-tree-1.c
> @@ -0,0 +1,90 @@
> +/* Execution test for conditional-compare chains built from AND/IOR trees of
> + comparisons. Each function is compared against a reference computed with
> + volatile operands so that it cannot be folded into the same code. */
> +
> +extern void abort (void);
> +
> +#define OPS int a, int b, int c, int d, int e, int f, int g, int h
> +#define ARGS a, b, c, d, e, f, g, h
> +
> +/* Balanced AND tree. */
> +static int __attribute__((noipa)) t1 (OPS)
> +{ return ((a < b) & (c < d)) & ((e < f) & (g < h)); }
> +
> +/* Balanced OR tree. */
> +static int __attribute__((noipa)) t2 (OPS)
> +{ return ((a < b) | (c < d)) | ((e < f) | (g < h)); }
> +
> +/* AND of an OR chain and an AND chain. */
> +static int __attribute__((noipa)) t3 (OPS)
> +{ return ((a < b) | (c < d)) & ((e < f) & (g < h)); }
> +
> +/* OR of an AND chain and an OR chain. */
> +static int __attribute__((noipa)) t4 (OPS)
> +{ return ((a < b) & (c < d)) | ((e < f) | (g < h)); }
> +
> +/* Deeper: eight leaves. */
> +static int __attribute__((noipa)) t5 (OPS)
> +{
> + return (((a < b) & (c < d)) & ((e < f) & (g < h)))
> + & (((a < c) & (b < d)) & ((e < g) & (f < h)));
> +}
> +
> +/* Mixed signed and unsigned leaves. */
> +static int __attribute__((noipa)) t6 (OPS)
> +{
> + return (((unsigned) a < (unsigned) b) & (c < d))
> + & ((e < f) & ((unsigned) g < (unsigned) h));
> +}
> +
> +/* Immediates, some inside and some outside the CCMP 5-bit range. */
> +static int __attribute__((noipa)) t7 (OPS)
> +{ return ((a == 3) & (b == 31)) & ((c == 32) & (d == -4)); }
> +
> +/* Reference versions. V forces separate evaluation of every leaf. */
> +#define V(x) ({ volatile int v_ = (x); v_; })
> +
> +static int r1 (OPS)
> +{ return (V(a < b) & V(c < d)) & (V(e < f) & V(g < h)); }
> +static int r2 (OPS)
> +{ return (V(a < b) | V(c < d)) | (V(e < f) | V(g < h)); }
> +static int r3 (OPS)
> +{ return (V(a < b) | V(c < d)) & (V(e < f) & V(g < h)); }
> +static int r4 (OPS)
> +{ return (V(a < b) & V(c < d)) | (V(e < f) | V(g < h)); }
> +static int r5 (OPS)
> +{
> + return ((V(a < b) & V(c < d)) & (V(e < f) & V(g < h)))
> + & ((V(a < c) & V(b < d)) & (V(e < g) & V(f < h)));
> +}
> +static int r6 (OPS)
> +{
> + return (V((unsigned) a < (unsigned) b) & V(c < d))
> + & (V(e < f) & V((unsigned) g < (unsigned) h));
> +}
> +static int r7 (OPS)
> +{ return (V(a == 3) & V(b == 31)) & (V(c == 32) & V(d == -4)); }
> +static const int vals[] = { -4, 0, 3, 31, 32, 33 };
> +#define NV ((int) (sizeof (vals) / sizeof (vals[0])))
> +
> +int
> +main (void)
> +{
> + for (int i0 = 0; i0 < NV; i0++)
> + for (int i1 = 0; i1 < NV; i1++)
> + for (int i2 = 0; i2 < NV; i2++)
> + for (int i3 = 0; i3 < NV; i3++)
> + {
> + int a = vals[i0], b = vals[i1], c = vals[i2], d = vals[i3];
> + int e = vals[i3], f = vals[i0], g = vals[i2], h = vals[i1];
> +
> + if (t1 (ARGS) != r1 (ARGS)) abort ();
> + if (t2 (ARGS) != r2 (ARGS)) abort ();
> + if (t3 (ARGS) != r3 (ARGS)) abort ();
> + if (t4 (ARGS) != r4 (ARGS)) abort ();
> + if (t5 (ARGS) != r5 (ARGS)) abort ();
> + if (t6 (ARGS) != r6 (ARGS)) abort ();
> + if (t7 (ARGS) != r7 (ARGS)) abort ();
> + }
> + return 0;
> +}
> diff --git a/gcc/testsuite/gcc.target/aarch64/ccmp_10.c
> b/gcc/testsuite/gcc.target/aarch64/ccmp_10.c
> new file mode 100644
> index 00000000000..b67b2410484
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/ccmp_10.c
> @@ -0,0 +1,33 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2 --param tree-reassoc-width=2
> -fdump-tree-reassoc2-details" } */
> +
> +/* A vector comparison does not set the condition flags, so a chain of them
> + is not a conditional compare sequence and keeps the parallel form. */
> +
> +typedef int v4si __attribute__ ((vector_size (16)));
> +
> +v4si
> +vector_chain (v4si a, v4si b, v4si c, v4si d, v4si e, v4si f, v4si g, v4si h)
> +{
> + return (a < b) & (c < d) & (e < f) & (g < h);
> +}
> +
> +/* A chain of scalar comparisons becomes one sequence instead. */
> +
> +int
> +scalar_chain (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return (a < b) & (c < d) & (e < f) & (g < h);
> +}
> +
> +/* A nested integer IOR is not a conditional compare sequence. */
> +
> +int
> +integer_ior (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return (a | b) & (c < d) & (e < f) & (g < h);
> +}
> +
> +/* { dg-final { scan-tree-dump-times "was chosen for reassociation" 1
> "reassoc2" } } */
> +/* { dg-final { scan-assembler-times {\tccmp\t} 5 } } */
> +/* { dg-final { scan-assembler-times {\tand\tw[0-9]+, w[0-9]+, w[0-9]+} 1 }
> } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/ccmp_12.c
> b/gcc/testsuite/gcc.target/aarch64/ccmp_12.c
> new file mode 100644
> index 00000000000..0d9fcb00c89
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/ccmp_12.c
> @@ -0,0 +1,17 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2 --param tree-reassoc-width=2
> -fdump-tree-reassoc2-details" } */
> +
> +/* Comparison leaves can have uses outside the chain. A chain stays linear
> + when another comparison can be folded into the flags. */
> +
> +volatile int result;
> +
> +int
> +one_multi_use (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + int x = a < b;
> + result = x;
> + return x & (c < d) & (e < f) & (g < h);
> +}
> +/* { dg-final { scan-assembler-times {\tccmp\t} 3 } } */
> +/* { dg-final { scan-tree-dump-not "was chosen for reassociation" "reassoc2"
> } } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/ccmp_14.c
> b/gcc/testsuite/gcc.target/aarch64/ccmp_14.c
> new file mode 100644
> index 00000000000..0ff0cd5ae7d
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/ccmp_14.c
> @@ -0,0 +1,23 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2 --param tree-reassoc-width=2
> -fdump-tree-reassoc2-details" } */
> +
> +/* If no comparison can be folded, the chain keeps the parallel form. */
> +
> +volatile int results[4];
> +
> +int
> +all_multi_use (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + int x0 = a < b;
> + int x1 = c < d;
> + int x2 = e < f;
> + int x3 = g < h;
> + results[0] = x0;
> + results[1] = x1;
> + results[2] = x2;
> + results[3] = x3;
> + return x0 & x1 & x2 & x3;
> +}
> +
> +/* { dg-final { scan-tree-dump-times "was chosen for reassociation" 1
> "reassoc2" } } */
> +/* { dg-final { scan-assembler-not {\tccmp\t} } } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/ccmp_6.c
> b/gcc/testsuite/gcc.target/aarch64/ccmp_6.c
> new file mode 100644
> index 00000000000..9feec51d8de
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/ccmp_6.c
> @@ -0,0 +1,63 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2" } */
> +/* { dg-final { check-function-bodies "**" "" } } */
> +
> +/* A chain of four comparisons is wide enough for reassociation to build a
> + balanced tree. Both source shapes have to reach expansion as one chain,
> + which a conditional compare sequence can hold in the flags. */
> +
> +/*
> +** and_chain:
> +** cmp w4, w5
> +** ccmp w6, w7, 0, lt
> +** ccmp w2, w3, 0, lt
> +** ccmp w0, w1, 0, lt
> +** cset w0, lt
> +** ret
> +*/
> +int
> +and_chain (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return (a < b) & (c < d) & (e < f) & (g < h);
> +}
> +
> +/*
> +** and_tree:
> +** cmp w4, w5
> +** ccmp w6, w7, 0, lt
> +** ccmp w2, w3, 0, lt
> +** ccmp w0, w1, 0, lt
> +** cset w0, lt
> +** ret
> +*/
> +int
> +and_tree (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return ((a < b) & (c < d)) & ((e < f) & (g < h));
> +}
> +
> +/*
> +** ior_tree:
> +** cmp w4, w5
> +** ccmp w6, w7, 1, ge
> +** ccmp w2, w3, 1, ge
> +** ccmp w0, w1, 1, ge
> +** cset w0, lt
> +** ret
> +*/
> +int
> +ior_tree (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return ((a < b) | (c < d)) | ((e < f) | (g < h));
> +}
> +
> +/* Both operands hold a value in the flags at the same time, so this one
> + needs two sequences and a bitwise operation. */
> +
> +int
> +and_of_ior (int a, int b, int c, int d, int e, int f, int g, int h)
> +{
> + return ((a < b) | (c < d)) & ((e < f) | (g < h));
> +}
> +
> +/* { dg-final { scan-assembler-times {\tand\tw[0-9]+, w[0-9]+, w[0-9]+} 1 }
> } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/fccmp_6.c
> b/gcc/testsuite/gcc.target/aarch64/fccmp_6.c
> new file mode 100644
> index 00000000000..5ded1304f03
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/fccmp_6.c
> @@ -0,0 +1,19 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2" } */
> +
> +/* A trapping comparison must start the sequence. Test every source
> + position in a chain that is long enough for parallel reassociation. */
> +
> +#define DEF(N, EXPR) \
> + int f##N (double a, double b, int c, int d, int e, int f, int g, int h) \
> + { return (EXPR); }
> +
> +DEF (0, (a < b) & (c < d) & (e < f) & (g < h))
> +DEF (1, (c < d) & (a < b) & (e < f) & (g < h))
> +DEF (2, (c < d) & (e < f) & (a < b) & (g < h))
> +DEF (3, (c < d) & (e < f) & (g < h) & (a < b))
> +
> +/* { dg-final { scan-assembler-times {\tfcmpe\t} 4 } } */
> +/* { dg-final { scan-assembler-times {\tccmp\t} 12 } } */
> +/* { dg-final { scan-assembler-times {\tcset\t} 4 } } */
> +/* { dg-final { scan-assembler-not {\tand\t} } } */
> diff --git a/gcc/testsuite/gcc.target/aarch64/fccmp_8.c
> b/gcc/testsuite/gcc.target/aarch64/fccmp_8.c
> new file mode 100644
> index 00000000000..1f49069b710
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/aarch64/fccmp_8.c
> @@ -0,0 +1,13 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2 --param tree-reassoc-width=2
> -fdump-tree-reassoc2-details" } */
> +
> +/* Two trapping comparisons cannot both start one sequence. */
> +
> +int
> +two_traps (double a, double b, double c, double d,
> + int e, int f, int g, int h)
> +{
> + return (a < b) & (c < d) & (e < f) & (g < h);
> +}
> +
> +/* { dg-final { scan-tree-dump-times "was chosen for reassociation" 1
> "reassoc2" } } */
> diff --git a/gcc/tree-ssa-reassoc.cc b/gcc/tree-ssa-reassoc.cc
> index dec64883891..737d30ba0a4 100644
> --- a/gcc/tree-ssa-reassoc.cc
> +++ b/gcc/tree-ssa-reassoc.cc
> @@ -5165,6 +5165,66 @@ swap_ops_for_binary_stmt (const vec<operand_entry *>
> &ops,
> std::swap (*oe1, *oe2);
> }
>
> +/* Return true if OP is set by a comparison in BB, which a conditional
> + compare can compute into the condition flags. */
> +
> +static bool
> +ccmp_comparison_p (tree op, basic_block bb)
> +{
> + if (TREE_CODE (op) != SSA_NAME)
> + return false;
> +
> + gimple *def = SSA_NAME_DEF_STMT (op);
> + if (!is_gimple_assign (def)
> + || gimple_bb (def) != bb
> + || TREE_CODE_CLASS (gimple_assign_rhs_code (def)) != tcc_comparison)
> + return false;
> +
> + /* A conditional compare sets the flags from one scalar comparison. */
> + machine_mode mode = TYPE_MODE (TREE_TYPE (gimple_assign_rhs1 (def)));
> + return ((SCALAR_INT_MODE_P (mode) || SCALAR_FLOAT_MODE_P (mode))
> + && known_le (GET_MODE_SIZE (mode), UNITS_PER_WORD));
While doing the newsletter I came across this patch. I think this is
incorrect. TImode could be done using ccmp (and is being worked on
right now). So having the check against UNITS_PER_WORD seems wrong.
If anything it should be against MAX_FIXED_MODE_SIZE .
> +}
> +
> +/* Return true if all operands in OPS form one conditional compare sequence
> + for CODE in BB. Put the operand that must start the sequence last in OPS,
> + where rewrite_expr_tree builds the innermost pair. Every operand must be
> a
> + comparison, and at least one must be foldable into the flags. */
> +
> +static bool
> +prepare_ccmp_chain (vec<operand_entry *> &ops, tree_code code, basic_block
> bb)
> +{
> + if (!targetm.have_ccmp ()
> + || (code != BIT_AND_EXPR && code != BIT_IOR_EXPR))
> + return false;
> +
> + bool seen_foldable = false;
> + int head = -1;
> +
> + for (unsigned int i = 0; i < ops.length (); ++i)
> + {
> + const operand_entry *oe = ops[i];
> + if (!ccmp_comparison_p (oe->op, bb))
> + return false;
> +
> + bool foldable = has_single_use (oe->op);
> + if (foldable
> + && gimple_could_trap_p (SSA_NAME_DEF_STMT (oe->op)))
> + {
> + if (head >= 0)
> + return false;
> + head = i;
> + }
> +
> + /* Comparison leaves can have uses outside this chain. */
> + seen_foldable |= foldable;
> + }
> +
> + if (head >= 0)
> + std::swap (*ops[head], *ops.last ());
> + return seen_foldable;
> +}
> +
> /* If definition of RHS1 or RHS2 dominates STMT, return the later of those
> two definitions, otherwise return STMT. Sets INSERT_BEFORE to indicate
> whether RHS1 op RHS2 can be inserted before or needs to be inserted
> @@ -7153,11 +7213,19 @@ reassociate_bb (basic_block bb)
> mult_num = rank_ops_for_fma (&ops);
> }
>
> + /* A chain that expands to a conditional compare sequence
> + has to stay linear. Every conditional compare tests the
> + flags that the previous comparison set, so the sequence
> + cannot be split into independent parts. */
> + bool ccmp_chain = (ops_num > 2
> + && prepare_ccmp_chain (ops, rhs_code,
> bb));
> +
> /* Only rewrite the expression tree to parallel in the
> last reassoc pass to avoid useless work back-and-forth
> with initial linearization. */
> bool has_fma = mult_num >= 2 && mult_num != ops_num;
> if (!reassoc_insert_powi_p
> + && !ccmp_chain
> && ops.length () > 3
> && (width = get_reassociation_width (&ops, mult_num,
> lhs,
> rhs_code, mode))
> @@ -7178,7 +7246,7 @@ reassociate_bb (basic_block bb)
> to make sure the ones that get the double
> binary op are chosen wisely. */
> int len = ops.length ();
> - if (!reassoc_insert_powi_p
> + if (!reassoc_insert_powi_p && !ccmp_chain
> && len >= 3
> && (!has_fma
> /* width > 1 means ranking ops results in better
> --
> 2.50.1 (Apple Git-155)