On Fri, Nov 13, 2026 at 6:44 AM <[email protected]> wrote:
>
> From: Richard Ball <[email protected]>
>
> The loop
>
> void add(int N, int *__restrict a, int *__restrict b, int *__restrict c) {
> for (int i = 0; i < N; i++) {
> c[i] = a[i] + b[i];
> if (i > 1000) {
> break;
> }
> }
> }
>
> is an example of a loop that we have started vectorizing using early break
> vectorization. However the given loop is not actually an early break loop as
> the condition on the exit introduces a MAX bound on N.
>
> This patch teaches ifcombine to identify such cases and rewrite the latch
> condition from i < N to i < MIN (N, C) where C is a constant.
First, this seems like it should be part of ivcanon pass rather than
ifcombine. Is there a reason why you can't move it to there?
Or maybe even loopinit ?
I can't tell if it reuses anything from ifcombine at all even so I
question why it is even part of this pass in the first place. Why not
its own or part of a loop one?
Thanks,
Andrea
>
> gcc/ChangeLog:
>
> PR tree-optimization/113134
> * tree-ssa-ifcombine.cc
> (struct ifcombine_exit_info): New struct.
> (ifcombine_exit_niter): New function.
> (ifcombine_reaches_header_via_latch_p): Likewise
> (ifcombine_removable_exit_p): Likewise.
> (ifcombine_collect_exit_bounds): Likewise.
> (ifcombine_rewrite_loop_exits): Likewise.
> (ifcombine_find_latch_exit): Likewise.
> (ifcombine_aggregate_safe_p): Likewise.
> (ifcombine_loop_exits): Likewise.
> (ifcombine_loop_exits_in_function): Likewise.
> (ifcombine_loop_exit_bounds): Likewise.
> (pass_tree_ifcombine::execute): Add call for new functions.
>
> gcc/testsuite/ChangeLog:
>
> PR tree-optimization/113134
> * gcc.dg/ifcombine-pr113134.c: New test.
> ---
> gcc/testsuite/gcc.dg/ifcombine-pr113134.c | 13 +
> gcc/tree-ssa-ifcombine.cc | 300 +++++++++++++++++++++-
> 2 files changed, 312 insertions(+), 1 deletion(-)
> create mode 100644 gcc/testsuite/gcc.dg/ifcombine-pr113134.c
>
> diff --git a/gcc/testsuite/gcc.dg/ifcombine-pr113134.c
> b/gcc/testsuite/gcc.dg/ifcombine-pr113134.c
> new file mode 100644
> index 00000000000..7a8d7c1e98b
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/ifcombine-pr113134.c
> @@ -0,0 +1,13 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -fdump-tree-ifcombine-details -save-temps" } */
> +
> +void add(int N, int *__restrict a, int *__restrict b, int *__restrict c) {
> + for (int i = 0; i < N; i++) {
> + c[i] = a[i] + b[i];
> + if (i > 1000) {
> + break;
> + }
> + }
> +}
> +
> +/* { dg-final { scan-tree-dump "ifcombine: aggregated loop" "ifcombine" } }
> */
> \ No newline at end of file
Watch out for missing newline at EOF.
> diff --git a/gcc/tree-ssa-ifcombine.cc b/gcc/tree-ssa-ifcombine.cc
> index d5e3d91e9b5..ad190427947 100644
> --- a/gcc/tree-ssa-ifcombine.cc
> +++ b/gcc/tree-ssa-ifcombine.cc
> @@ -45,6 +45,11 @@ along with GCC; see the file COPYING3. If not see
> #include "asan.h"
> #include "bitmap.h"
> #include "cfgloop.h"
> +#include "tree-ssa-loop.h"
> +#include "tree-ssa-loop-niter.h"
> +#include "tree-ssa-loop-ivopts.h"
> +#include "tree-scalar-evolution.h"
> +#include "tree-chrec.h"
>
> #ifndef LOGICAL_OP_NON_SHORT_CIRCUIT
> #define LOGICAL_OP_NON_SHORT_CIRCUIT \
> @@ -523,6 +528,295 @@ ifcombine_rewrite_to_defined_overflow
> (gimple_stmt_iterator gsi)
> rewrite_to_defined_unconditional (&gsi);
> }
>
> +/* Information retained for each exit incorporated in the aggregate. */
> +struct ifcombine_exit_info
> +{
> + edge exit;
> + tree niter;
> +};
> +
> +/* Return the exit iteration count, accounting for an immediate exit. */
> +static tree
> +ifcombine_exit_niter (class loop *loop, edge exit)
> +{
> + tree_niter_desc desc;
> +
> + if (!number_of_iterations_exit (loop, exit, &desc, false))
> + return NULL_TREE;
> +
> + tree niter = desc.niter;
> + if (!niter || chrec_contains_undetermined (niter))
> + return NULL_TREE;
> +
> + if (integer_nonzerop (desc.may_be_zero))
> + return build_zero_cst (TREE_TYPE (niter));
> +
> + if (!integer_zerop (desc.may_be_zero))
> + niter = fold_build3 (COND_EXPR, TREE_TYPE (niter),
> + desc.may_be_zero,
> + build_zero_cst (TREE_TYPE (niter)), niter);
> +
> + return niter;
> +}
> +
> +/* Require the non-exiting edge of E to reach the controlling header
> + via the loop latch. */
> +static bool
> +ifcombine_reaches_header_via_latch_p (class loop *loop,
> + edge e, edge controlling_exit)
> +{
> + if (EDGE_COUNT (e->src->succs) != 2)
> + return false;
> +
> + edge nonexit = EDGE_SUCC (e->src, 0);
> + if (nonexit == e)
> + nonexit = EDGE_SUCC (e->src, 1);
> +
> + basic_block bb = nonexit->dest;
> + return (bb == loop->latch
> + && single_pred_p (bb)
> + && single_pred_edge (bb) == nonexit
> + && single_succ_p (bb)
> + && single_succ (bb) == controlling_exit->src
> + && controlling_exit->src == loop->header);
> +}
> +
> +/* Check a candidate early exit's CFG shape and scalar exit PHIs.
> + Virtual PHIs are not compared; */
> +static bool
> +ifcombine_removable_exit_p (class loop *loop, edge e,
> + edge controlling_exit)
> +{
> + if (e == controlling_exit
> + || !just_once_each_iteration_p (loop, e->src)
> + || !dominated_by_p (CDI_DOMINATORS,
> + e->src, controlling_exit->src)
> + || e->dest != controlling_exit->dest
> + || EDGE_COUNT (e->src->succs) != 2
> + || !ifcombine_reaches_header_via_latch_p (loop, e,
> + controlling_exit))
> + return false;
> +
> + for (gphi_iterator gpi = gsi_start_phis (e->dest);
> + !gsi_end_p (gpi); gsi_next (&gpi))
> + {
> + gphi *phi = gpi.phi ();
> + if (virtual_operand_p (gimple_phi_result (phi)))
> + continue;
> +
> + tree early_arg = PHI_ARG_DEF_FROM_EDGE (phi, e);
> + tree control_arg = PHI_ARG_DEF_FROM_EDGE (phi, controlling_exit);
> + if (!operand_equal_for_phi_arg_p (early_arg, control_arg))
> + return false;
> + }
> +
> + return true;
> +}
> +
> +/* Find eligible exits and form the minimum controlling bound. */
> +static bool
> +ifcombine_collect_exit_bounds (class loop *loop,
> + edge controlling_exit,
> + tree *aggregate_niter,
> + auto_vec<ifcombine_exit_info> &candidates)
> +{
> + tree controlling_niter = ifcombine_exit_niter (loop, controlling_exit);
> + if (!controlling_niter)
> + return false;
> +
> + tree niter = controlling_niter;
> + auto_vec<edge> exits = get_loop_exit_edges (loop);
> +
> + for (edge e : exits)
> + {
> + if (e == controlling_exit
> + || !just_once_each_iteration_p (loop, e->src)
> + || !dominated_by_p (CDI_DOMINATORS,
> + e->src, controlling_exit->src))
> + continue;
> +
> + tree exit_niter = ifcombine_exit_niter (loop, e);
> + if (!exit_niter
> + || TREE_TYPE (exit_niter) != TREE_TYPE (controlling_niter)
> + || !ifcombine_removable_exit_p (loop, e, controlling_exit))
> + continue;
> +
> + tree type = TREE_TYPE (controlling_niter);
> + if (!TYPE_UNSIGNED (type)
> + || TREE_CODE (exit_niter) != INTEGER_CST)
> + continue;
> +
> + wide_int value = wi::to_wide (exit_niter);
> + if (wi::lts_p (value, 0)
> + || wi::geu_p (wi::to_wide (exit_niter),
> + wi::to_wide (TYPE_MAX_VALUE (type))))
> + continue;
> +
> + tree adjusted_niter = fold_convert (type, exit_niter);
> + adjusted_niter = int_const_binop (PLUS_EXPR, adjusted_niter,
> + build_int_cst (type, 1));
> +
> + ifcombine_exit_info info = { e, exit_niter };
> + candidates.safe_push (info);
> + niter = fold_build2 (MIN_EXPR, type, niter, adjusted_niter);
> + }
> +
> + if (candidates.is_empty ())
> + return false;
> +
> + *aggregate_niter = niter;
> + return true;
> +}
> +
> +/* Replace the controlling exit with a canonical IV test, then
> + make the selected early exit conditions always take the non-exit
> + path. */
> +static bool
> +ifcombine_rewrite_loop_exits (class loop *loop, edge controlling_exit,
> + tree aggregate_niter,
> + auto_vec<ifcombine_exit_info> &candidates)
> +{
> + create_canonical_iv (loop, controlling_exit, aggregate_niter);
> +
> + for (unsigned i = 0; i < candidates.length (); ++i)
> + {
> + edge e = candidates[i].exit;
> + gcond *cond = as_a<gcond *> (*gsi_last_nondebug_bb (e->src));
> +
> + if (e->flags & EDGE_TRUE_VALUE)
> + gimple_cond_make_false (cond);
> + else
> + gimple_cond_make_true (cond);
> +
> + update_stmt (cond);
> + }
> +
> + if (dump_file && (dump_flags & TDF_DETAILS))
> + fprintf (dump_file, "ifcombine: aggregated loop %d exits\n",
> + loop->num);
> +
> + return true;
> +}
> +
> +/* Locate a conditional loop-header exit. */
> +static edge
> +ifcombine_find_latch_exit (class loop *loop)
> +{
> + basic_block header = loop->header;
> + if (EDGE_COUNT (header->succs) != 2)
> + return NULL;
> +
> + gimple_stmt_iterator gsi = gsi_last_nondebug_bb (header);
> + if (gsi_end_p (gsi)
> + || gimple_code (gsi_stmt (gsi)) != GIMPLE_COND)
> + return NULL;
> +
> + edge e0 = EDGE_SUCC (header, 0);
> + edge e1 = EDGE_SUCC (header, 1);
> +
> + if (loop_exit_edge_p (loop, e0)
> + && flow_bb_inside_loop_p (loop, e1->dest))
> + return e0;
> + if (loop_exit_edge_p (loop, e1)
> + && flow_bb_inside_loop_p (loop, e0->dest))
> + return e1;
> +
> + return NULL;
> +}
> +
> +/* Check aggregate structural constraints. */
> +static bool
> +ifcombine_aggregate_safe_p (class loop *loop, edge controlling_exit,
> + tree aggregate_niter,
> + auto_vec<ifcombine_exit_info> &candidates)
> +{
> + if (!aggregate_niter
> + || chrec_contains_undetermined (aggregate_niter)
> + || candidates.is_empty ()
> + || !loop_preheader_edge (loop)
> + || controlling_exit->src != loop->header
> + || !TYPE_UNSIGNED (TREE_TYPE (aggregate_niter)))
> + return false;
> +
> + for (unsigned i = 0; i < candidates.length (); ++i)
> + {
> + edge e = candidates[i].exit;
> + if (e == controlling_exit
> + || e->dest != controlling_exit->dest
> + || !ifcombine_reaches_header_via_latch_p (loop, e,
> + controlling_exit))
> + return false;
> + }
> +
> + return true;
> +}
> +
> +/* Analyze and, if eligible, transform one loop. */
> +static bool
> +ifcombine_loop_exits (class loop *loop)
> +{
> + edge controlling_exit = ifcombine_find_latch_exit (loop);
> + if (!controlling_exit)
> + return false;
> +
> + tree aggregate_niter = NULL_TREE;
> + auto_vec<ifcombine_exit_info> candidates;
> +
> + if (!ifcombine_collect_exit_bounds (loop, controlling_exit,
> + &aggregate_niter, candidates)
> + || !ifcombine_aggregate_safe_p (loop, controlling_exit,
> + aggregate_niter, candidates))
> + return false;
> +
> + return ifcombine_rewrite_loop_exits (loop, controlling_exit,
> + aggregate_niter, candidates);
> +}
> +
> +/* Process loops from the innermost out, invalidating SCEV after
> + each successful rewrite. */
> +static bool
> +ifcombine_loop_exits_in_function (function *fun)
> +{
> + bool changed = false;
> + for (auto loop : loops_list (fun, LI_FROM_INNERMOST))
> + {
> + if (loop->num == 0)
> + continue;
> +
> + if (ifcombine_loop_exits (loop))
> + {
> + changed = true;
> + scev_reset ();
> + }
> + }
> +
> + return changed;
> +}
> +
> +/* Set up SCEV infrastructure if necessary, process loops, and
> + restore the analysis state. */
> +static bool
> +ifcombine_loop_exit_bounds (function *fun)
> +{
> + bool in_loop_pipeline = scev_initialized_p ();
> + if (!in_loop_pipeline)
> + {
> + loop_optimizer_init (LOOPS_NORMAL | LOOPS_HAVE_RECORDED_EXITS);
> + scev_initialize ();
> + }
> +
> + bool changed = false;
> + if (number_of_loops (fun) > 1)
> + changed = ifcombine_loop_exits_in_function (fun);
> +
> + if (!in_loop_pipeline)
> + {
> + scev_finalize ();
> + loop_optimizer_finalize ();
> + }
> +
> + return changed;
> +}
>
> /* Replace the conditions in INNER_COND and OUTER_COND with COND and COND2.
> COND and COND2 are computed for insertion at INNER_COND, with OUTER_COND
> @@ -1556,7 +1850,11 @@ pass_tree_ifcombine::execute (function *fun)
>
> free (bbs);
>
> - return cfg_changed ? TODO_cleanup_cfg : 0;
> + /* Combine loop exits with compatible iteration bounds. */
> + bool loop_changed = ifcombine_loop_exit_bounds (fun);
> +
> + return (cfg_changed || loop_changed)
> + ? TODO_cleanup_cfg : 0;
> }
>
> } // anon namespace
> --
> 2.34.1
>