Re: [PATCH v3] gcc/tree-ssa-phiopt.cc: add canonicalize_phi_constants [PR71336]
Andrea Pinski <[email protected]>
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <CALvbMcB=J6eg=Q_VdukNvKE2h3pUpuTNsAEG6MUn=2PSL27JyQ@mail.gmail.com> |
On Sun, Jul 26, 2026 at 2:37 AM Daniel Barboza <[email protected]> wrote: > > This was done previously in match.pd ([1] is the last match.pd version) > but I grew to dislike how that turned out. We we relying on assumptions > like "we are counting on something else to undo the transformation we > are doing since it would be better". The reason is that, in match.pd, we > can't capture the resulting LHS of the simplification to run checks on > it (e.g. if it's single use). In particular we want to detect if the > phi result would be used in 122608 and do not get in the way. We can > do these checks in the passes though, thus I decided to move the logic > out of match.pd. > > The idea is the same: identify cases where a zero_one comparison is used > to conditional constant assignment and turn that into an unconditional > PLUS. For the code in PR71336: > > int test(int a) { > return a & 1 ? 7 : 3; > } > > We'll turn that into "(a&1) * (7 - 3) + 3", which yields the same > results but without the conditional, promoving more optimization > opportunities. We're also handling the "zero_one EQ 0" variant by > canonicalizing it to "zero_one NE 0". Thinking about this is we really want this in expand and/or ifcvt rather than gimple I think. I do think simple: if (a != 0) b = c; else b = d; Should be converted into: t = a != 0; b = t ? c : d; during the last phiopt. But that requires us to add more to expand to support all of the tricks. From the old review of my patch to add some of the tricks to expand, RTH mentioned the constant cases should be left up to the target but that was ~10 years ago. I am suspect it would be best if it is left up to the targets for constants though we need to start adding tricks to the target movcc patterns then. > > x86_64 and aarch64 tests show better code all around. riscv64 shows > code improvement in the NE pattern but a small code regression in the EQ > case. Jeff is aware of it and told that we should go ahead with the > transformation since other targets benefit from it. > > Bootstrapped and regression tested in x86_64, aarch64 and riscv64. > > [1] https://gcc.gnu.org/pipermail/gcc-patches/2026-March/710154.html > > PR tree-optimization/71336 > > gcc/ChangeLog: > > * tree-ssa-phiopt.cc (canonicalize_phi_constants): transform > "zero_one NE|EQ 0 ? CST1 : CST2" into "CST + zero_one*diff". > (pass_phiopt::execute): use canonicalize_phi_constants. > > gcc/testsuite/ChangeLog: > > * gcc.dg/tree-ssa/pr71336-2.c: New test. > * gcc.dg/tree-ssa/pr71336.c: New test. > --- > > Changes from v2: > - changed from match.pd to tree-ssa-phiopt; > - v2 link: https://gcc.gnu.org/pipermail/gcc-patches/2026-March/710154.html > > gcc/testsuite/gcc.dg/tree-ssa/pr71336-2.c | 63 +++++++ > gcc/testsuite/gcc.dg/tree-ssa/pr71336.c | 22 +++ > gcc/tree-ssa-phiopt.cc | 198 ++++++++++++++++++++++ > 3 files changed, 283 insertions(+) > create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/pr71336-2.c > create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/pr71336.c > > diff --git a/gcc/testsuite/gcc.dg/tree-ssa/pr71336-2.c b/gcc/testsuite/gcc.dg/tree-ssa/pr71336-2.c > new file mode 100644 > index 00000000000..3af80507234 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/tree-ssa/pr71336-2.c > @@ -0,0 +1,63 @@ > +/* { dg-do run } */ > +/* { dg-options "-O1" } */ > + > +/* Macro adapted from builtin-object-size-common.h */ > +#define FAIL() \ > + do { \ > + __builtin_printf ("Failure at line: %d\n", __LINE__); \ > + abort(); \ > + } while (0) > + > +void abort(void); > + > +int test (int a) { > + return a & 1 ? 7 : 3; > +} > + > +int test2 (int a) { > + return a & 1 ? 3 : 7; > +} > + > +int test3 (int a) { > + return (a & 1) == 0 ? 3 : 7; > +} > + > +int test4 (int a) { > + return (a & 1) == 0 ? 7 : 3; > +} > + > +int main (void) { > + /* a & 1 ? 7 : 3; */ > + if (test (0) != 3) > + FAIL (); > + if (test (1) != 7) > + FAIL (); > + if (test (3) != 7) > + FAIL (); > + > + /* a & 1 ? 3 : 7; */ > + if (test2 (0) != 7) > + FAIL (); > + if (test2 (1) != 3) > + FAIL (); > + if (test2 (3) != 3) > + FAIL (); > + > + /* (a & 1) == 0 ? 3 : 7; */ > + if (test3 (0) != 3) > + FAIL (); > + if (test3 (1) != 7) > + FAIL (); > + if (test3 (2) != 3) > + FAIL (); > + > + /* (a & 1) == 0 ? 7 : 3; */ > + if (test4 (0) != 7) > + FAIL (); > + if (test4 (1) != 3) > + FAIL (); > + if (test4 (2) != 7) > + FAIL (); > + > + return 0; > +} > \ No newline at end of file > diff --git a/gcc/testsuite/gcc.dg/tree-ssa/pr71336.c b/gcc/testsuite/gcc.dg/tree-ssa/pr71336.c > new file mode 100644 > index 00000000000..1a0c30baf70 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/tree-ssa/pr71336.c > @@ -0,0 +1,22 @@ > +/* { dg-additional-options -O1 } */ > +/* { dg-additional-options -fdump-tree-optimized } */ > + > +int test (int a) { > + return a & 1 ? 17 : 3; > +} > + > +int test2 (int a) { > + return (a & 1) == 0 ? 17 : 3; > +} > + > +int test3 (int a) { > + return a & 1 ? 3 : 17; > +} > + > +int test4 (int a) { > + return (a & 1) == 0 ? 3 : 17; > +} > + > +/* { dg-final { scan-tree-dump-times " \\* " 4 optimized } } */ > +/* { dg-final { scan-tree-dump-times " goto " 0 optimized } } */ > + > \ No newline at end of file > diff --git a/gcc/tree-ssa-phiopt.cc b/gcc/tree-ssa-phiopt.cc > index d48f164c7fb..f209c673f15 100644 > --- a/gcc/tree-ssa-phiopt.cc > +++ b/gcc/tree-ssa-phiopt.cc > @@ -3906,6 +3906,198 @@ cond_if_else_store_replacement (basic_block then_bb, basic_block else_bb, > return ok; > } > > +/* Given a PHI with 2 edges, both with CST args and an > + empty middle_bb, with a zero_one NE zero comparison: > + > + <bb 2> > + if (zero_one != 0) goto <bb 4> else goto <bb 3> > + <bb 3> > + goto <bb 4> > + <bb 4> > + c_12 = PHI <3 (3), 17 (2)> > + > + Canonizalize it into a PHI <0,1> that recreates the CSTs > + with a MULT: > + > + <bb 2> > + if (zero_one != 0) goto <bb 4> else goto <bb 3> > + <bb 3> > + goto <bb 4> > + <bb 4> > + c_12 = PHI <0 (3), 1 (2)> > + _ssa1 = c_12 * (17 - 3) > + _ssa2 = 3 + _ssa1 > + (replace c_12 with _ssa2) > + > + This allows the following phiopt pass (via match_simplify_replacement) > + to eliminate the gcond and the middle_bb, resulting in better code > + generation. > + > + EQ comparisons are also supported and will canonicalized to NE. > + > + The canonicalization is restricted to zero_one comparisons because > + match.pd has forwprop transformations such as: > + > + (m1 cmp m2) * d => (m1 cmp m2) ? d : 0 > + > + That would end up undoing the canonicalization done here. */ > +static bool > +canonicalize_phi_constants (basic_block cond_bb, gphi *phi, > + tree arg0, tree arg1, > + edge e0, edge e1) > +{ > + if (gimple_phi_num_args (phi) != 2 > + || TREE_CODE (arg0) != INTEGER_CST > + || TREE_CODE (arg1) != INTEGER_CST > + || tree_int_cst_sgn (arg0) <= 0 > + || tree_int_cst_sgn (arg1) <= 0 > + || !tree_fits_uhwi_p (arg0) > + || !tree_fits_uhwi_p (arg1) > + || (tree_to_uhwi (arg0) == tree_to_uhwi (arg1))) > + return false; > + > + tree phires = gimple_phi_result (phi); > + > + /* ??? Do we need to check both virtual_operand_p and !INTEGRAL_TYPE_P? */ > + if (virtual_operand_p (phires) > + || !INTEGRAL_TYPE_P (TREE_TYPE (phires))) > + return false; > + > + /* Check if phi_res is single use and not used in any > + binary operation. We make this check to avoid getting > + in the way of simplifications such as PR 122608 that > + are better. */ > + use_operand_p use; > + gimple *use_stmt; > + if (!single_imm_use (phires, &use, &use_stmt) > + || (!is_a<gassign*> (use_stmt) > + && !is_a<greturn*> (use_stmt)) > + || (is_a<gassign*> (use_stmt) > + && get_gimple_rhs_class ( > + gimple_assign_rhs_code (use_stmt)) == GIMPLE_BINARY_RHS)) > + return false; > + > + gcond *cond = as_a <gcond *> (*gsi_last_bb (cond_bb)); > + tree_code cond_code = gimple_cond_code (cond); > + tree zero_one = gimple_cond_lhs (cond); > + if (!tree_zero_one_valued_p (zero_one) > + || !INTEGRAL_TYPE_P (TREE_TYPE (zero_one)) > + || !integer_zerop (gimple_cond_rhs (cond)) > + || (cond_code != NE_EXPR && cond_code != EQ_EXPR)) > + return false; > + > + /* At this point we're committed. What we want now is: > + - if we have an EQ_EXPR canonicalize it to NE_EXPR to > + simplify the logic; > + - add a "new_phires = PHI <0, 1>;" gphi; > + - add a "new_phires * (CST_GT - CST_LT)" stmt; > + - add a "CST PLUS (new_phires*diff)" stmt; > + - replace phires with new_phires and remove the old PHI. */ > + > + bool cond_canonicalized = false; > + if (cond_code == EQ_EXPR) > + { > + gimple_cond_set_code (cond, NE_EXPR); > + std::swap (arg0, arg1); > + cond_canonicalized = true; > + } > + > + unsigned HOST_WIDE_INT diff = 0; > + unsigned HOST_WIDE_INT arg0_val = tree_to_uhwi (arg0); > + unsigned HOST_WIDE_INT arg1_val = tree_to_uhwi (arg1); > + bool arg0_gt = false; > + > + if (arg0_val > arg1_val) > + { > + diff = arg0_val - arg1_val; > + arg0_gt = true; > + } > + else > + diff = arg1_val - arg0_val; > + > + bool e0_true_edge = false; > + if (e0->flags & EDGE_TRUE_VALUE) > + e0_true_edge = true; > + > + tree cst_stmt_operand; > + > + /* zero_one NE 0 ? CST_GT : CST_LT will be reduced to > + CST_LT + zero_one*diff; */ > + if ((e0_true_edge && arg0_gt) > + || (!e0_true_edge && !arg0_gt)) > + { > + cst_stmt_operand = arg0_gt ? arg1 : arg0; > + } > + /* zero_one NE 0 ? CST_LT : CST_GT will be reduced to > + CST_GT + zero_one*(-diff); */ > + else if ((e0_true_edge && !arg0_gt) > + || (!e0_true_edge && arg0_gt)) > + { > + diff = -diff; > + cst_stmt_operand = arg0_gt ? arg0 : arg1; > + } > + else > + gcc_unreachable (); > + > + tree elems_type = TREE_TYPE (cst_stmt_operand); > + tree new_phires = make_ssa_name (elems_type, NULL); > + gphi *new_phi = create_phi_node (new_phires, phi->bb); > + > + /* For zero_one NE 0 ? CST1 : CST2, zero_one == 1 > + in the 'true' edge. */ > + tree e0_arg, e1_arg; > + if (e0_true_edge) > + { > + e0_arg = build_int_cst (elems_type, 1); > + e1_arg = build_int_cst (elems_type, 0); > + } > + else > + { > + e0_arg = build_int_cst (elems_type, 0); > + e1_arg = build_int_cst (elems_type, 1); > + } > + > + SET_PHI_ARG_DEF (new_phi, e0->dest_idx, e0_arg); > + SET_PHI_ARG_DEF (new_phi, e1->dest_idx, e1_arg); > + > + gimple_seq seq = nullptr; > + > + /* new_phires * diff stmt. */ > + tree mult_lhs = gimple_build (&seq, MULT_EXPR, elems_type, > + new_phires, build_int_cst (elems_type, diff)); > + /* CST PLUS (new_phires*diff) stmt. */ > + tree cst_lhs = gimple_build (&seq, PLUS_EXPR, elems_type, > + cst_stmt_operand, mult_lhs); > + > + /* In theory we could do replace_phi_edge_with_variable here and > + be done with it but bootstrap really dislikes that. In the > + next phiopt pass match_simplify_replacement will replace the > + phi and make a better job at it, so for now we're happy > + with just adjusting the new PHI and the extra stmts. */ > + gimple_stmt_iterator gsi = gsi_start_bb (phi->bb); > + gsi_insert_seq_before (&gsi, seq, GSI_CONTINUE_LINKING); > + > + replace_uses_by (phires, cst_lhs); > + > + gsi = gsi_for_phi (phi); > + gsi_remove (&gsi, true); > + > + statistics_counter_event (cfun, "Canonicalized PHI constant args", 1); > + if (dump_file && (dump_flags & TDF_DETAILS)) > + { > + if (cond_canonicalized) > + fprintf (dump_file, > + "COND_EXPR in block %d canonicalized to NE_EXPR. ", > + cond_bb->index); > + > + fprintf (dump_file, > + "Constant PHI args in block %d canonicalized to 0/1.\n", > + new_phi->bb->index); > + } > + > + return true; > +} > + > /* If PHI at MERGE is a "load PHI", PHI <*P, *Q> whose two arguments are > single-use, non-volatile scalar MEM_REF loads reading the same memory state > (same VUSE), factor the load out: introduce P' = PHI <P, Q> and a single > @@ -4756,6 +4948,12 @@ pass_phiopt::execute (function *) > && !diamond_p > && spaceship_replacement (bb, bb1, e1, e2, phi, arg0, arg1)) > cfgchanged = true; > + else if (!early_p > + && !diamond_p > + && single_pred_p (bb1) > + && empty_block_p (bb1) > + && canonicalize_phi_constants (bb, phi, arg0, arg1, e1, e2)) > + cfgchanged = true; > }; > > execute_over_cond_phis (phiopt_exec); > -- > 2.43.0 >