On Wed, 2 Sep 2026, Richard Biener wrote:
> iv_elimination_compare_lt turns a loop with the exit test i < b, entered
> with i = a and a pointer p = base + a stepping in lockstep with i, into a
> loop with the exit test p < p_0 - a + b, so as to eliminate the counter.
>
> This is unsound in two ways when the loop may roll zero times, i.e. when
> the number of iterations is a + 1 > b ? 0 : b - a - 1.
>
> The first one is that the bound is not recomputed by the function, but
> taken from cand_value_at, which evaluates the candidate at the number of
> iterations, i.e. computes p_0 + (b - a - 1 + 1) with b - a computed in the
> type of the number of iterations. If that type is narrower than the type
> of the offsets, converting the difference is not value preserving when the
> loop rolls zero times, since b - a is negative there. For example, with
>
> void f (char *p, unsigned int i, unsigned int n)
> { p += i; do { *p = 1; p += 1; i++; } while (i < n); }
>
> called as f (p, 2, 0), the bound is computed as p_0 + (sizetype) 0xfffffffd
> and the loop walks 4G bytes forward instead of exiting at the first test.
>
> The second one is that the comparison of the pointers is performed modulo
> the size of the address space, so replacing i < b by p_0 - a + i < p_0 - a
> + b requires the addition of p_0 - a to be order preserving on the values
> of i and b. That does not hold when the offsets are as wide as a pointer
> and either of them is large, e.g. for
>
> void f (char *p, uintptr_t i, uintptr_t n)
> { p += i; do { *p = 1; p += 1; i++; } while (i < n); }
>
> called as f (a + 2, -2, 8): the bound p_0 - a + b wraps around to the very
> end of the address space, the test holds and the loop again runs away.
>
> So recompute the bound as base + step * b - step * a, converting a and b to
> the offset type separately, and require the may_be_zero comparison to be an
> unsigned one and both scaled offsets to be provably non-negative and free of
> overflow. Since the recomputed bound is the value of the candidate after
> the last iteration, also require the exit test to come after the increment
> of the candidate. Finally, move the expression_expensive_p check on the
> bound after the call so that it applies to the recomputed bound.
>
> Assisted-by: Claude Opus 5 <[email protected]>
>
> Bootstrapped and tested on x86_64-unknown-linux-gnu.
>
> Instead of removing iv_elimination_compare_lt alltogether as suggested
> after my 2nd attempt of deciphering it a few months ago this is what
> I arrived at with some LLM help and back-and-forth.
>
> OK? Otherwise I'll proceed with purging the whole thing, the
> restriction on integer-width < pointer-width makes it less useful,
> esp. on 32bit archs.
I have re-tested this on x86_64-unknwon-linux-gnu and plan to push
this tomorrow.
Thanks,
Richard.
> Thanks,
> Richard.
>
> PR tree-optimization/113703
> * tree-ssa-loop-ivopts.cc: Include gimple-range.h.
> (nonneg_scaled_offset_p): New function.
> (iv_elimination_compare_lt): Add USE and BOUND_P parameters. Bail
> out if the exit test does not come after the increment of the
> candidate, if the comparison in the number of iterations is signed
> or if the offsets are not known to be non-negative and to not
> overflow. Recompute the bound and store it in BOUND_P.
> (may_eliminate_iv): Adjust the call to iv_elimination_compare_lt
> and move the expensiveness check on the bound after it.
>
> * gcc.dg/tree-ssa/ivopts-lt.c: Use unsigned int counters,
> restrict to lp64.
> * gcc.dg/tree-ssa/ivopts-lt-2.c: Restrict to lp64.
> * gcc.dg/torture/pr113703-1.c: New test.
> * gcc.dg/torture/pr113703-2.c: New test.
> ---
> gcc/testsuite/gcc.dg/torture/pr113703-1.c | 34 ++++++
> gcc/testsuite/gcc.dg/torture/pr113703-2.c | 37 ++++++
> gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt-2.c | 2 +-
> gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt.c | 10 +-
> gcc/tree-ssa-loop-ivopts.cc | 118 ++++++++++++++++----
> 5 files changed, 173 insertions(+), 28 deletions(-)
> create mode 100644 gcc/testsuite/gcc.dg/torture/pr113703-1.c
> create mode 100644 gcc/testsuite/gcc.dg/torture/pr113703-2.c
>
> diff --git a/gcc/testsuite/gcc.dg/torture/pr113703-1.c
> b/gcc/testsuite/gcc.dg/torture/pr113703-1.c
> new file mode 100644
> index 00000000000..6446d32c039
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/torture/pr113703-1.c
> @@ -0,0 +1,34 @@
> +/* { dg-do run } */
> +/* { dg-additional-options "-fno-tree-vectorize
> -fno-tree-loop-distribute-patterns" } */
> +
> +#include <stdint.h>
> +
> +char a[16];
> +
> +__attribute__((noipa)) void
> +f (char *p, uintptr_t i, uintptr_t n)
> +{
> + p += i;
> + do
> + {
> + *p = 1;
> + p += 1;
> + i++;
> + }
> + while (i < n);
> +}
> +
> +int
> +main ()
> +{
> + /* I is -2 and thus not less than N, so the loop stores to A[0] only.
> + IVOPTs used to replace the exit test by P < (A + 2) + N, which does
> + not hold at the first test and makes the loop iterate on. */
> + f (a + 2, -2, 8);
> + if (a[0] != 1)
> + __builtin_abort ();
> + for (int j = 1; j < 16; j++)
> + if (a[j] != 0)
> + __builtin_abort ();
> + return 0;
> +}
> diff --git a/gcc/testsuite/gcc.dg/torture/pr113703-2.c
> b/gcc/testsuite/gcc.dg/torture/pr113703-2.c
> new file mode 100644
> index 00000000000..0d93be206d7
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/torture/pr113703-2.c
> @@ -0,0 +1,37 @@
> +/* { dg-do run { target *-*-linux* *-*-gnu* *-*-uclinux* } } */
> +/* { dg-additional-options "-fno-tree-vectorize
> -fno-tree-loop-distribute-patterns" } */
> +
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +__attribute__((noipa)) void
> +f (char *p, unsigned int i, unsigned int n)
> +{
> + p += i;
> + do
> + {
> + *p = 1;
> + p += 1;
> + i++;
> + }
> + while (i < n);
> +}
> +
> +int
> +main ()
> +{
> + long pgsz = sysconf (_SC_PAGESIZE);
> + char *p = mmap (NULL, pgsz * 2, PROT_READ | PROT_WRITE,
> + MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
> + if (p == MAP_FAILED)
> + return 0;
> + mprotect (p + pgsz, pgsz, PROT_NONE);
> + /* I + 1 is not less than N, so the loop stores to P[4] only. IVOPTs
> + used to compute the bound of the transformed loop from the number of
> + iterations N - I - 1, which is (unsigned int) -3 here and gets zero
> + extended, so that the loop ran into the protected page. */
> + f (p + 2, 2, 0);
> + if (p[4] != 1 || p[5] != 0)
> + __builtin_abort ();
> + return 0;
> +}
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt-2.c
> b/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt-2.c
> index be325775fbb..4d244ddadea 100644
> --- a/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt-2.c
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt-2.c
> @@ -1,4 +1,4 @@
> -/* { dg-do compile } */
> +/* { dg-do compile { target lp64 } } */
> /* { dg-options "-O2 -fno-tree-vectorize -fno-tree-loop-distribute-patterns
> -fdump-tree-ivopts" } */
> /* { dg-skip-if "PR68644" { hppa*-*-* powerpc*-*-* } } */
>
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt.c
> b/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt.c
> index 8d2b9d39355..2ee43d12567 100644
> --- a/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt.c
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/ivopts-lt.c
> @@ -1,11 +1,13 @@
> -/* { dg-do compile } */
> +/* { dg-do compile { target lp64 } } */
> /* { dg-options "-O2 -fno-tree-vectorize -fno-tree-loop-distribute-patterns
> -fdump-tree-ivopts" } */
> -/* { dg-require-effective-target stdint_types } */
>
> -#include "stdint.h"
> +/* The counter can be eliminated in favour of P, since the offsets I and N
> + are known to be non-negative and small enough not to overflow. Note that
> + this is not the case for offsets of the width of a pointer, see
> + gcc.dg/torture/ivopts-lt-1.c. */
>
> void
> -f1 (char *p, uintptr_t i, uintptr_t n)
> +f1 (char *p, unsigned int i, unsigned int n)
> {
> p += i;
> do
> diff --git a/gcc/tree-ssa-loop-ivopts.cc b/gcc/tree-ssa-loop-ivopts.cc
> index 5a4254290d8..fa3c2c136fc 100644
> --- a/gcc/tree-ssa-loop-ivopts.cc
> +++ b/gcc/tree-ssa-loop-ivopts.cc
> @@ -133,6 +133,7 @@ along with GCC; see the file COPYING3. If not see
> #include "dbgcnt.h"
> #include "cfganal.h"
> #include "gimple-fold.h"
> +#include "gimple-range.h"
>
> /* For lang_hooks.types.type_for_mode. */
> #include "langhooks.h"
> @@ -5234,14 +5235,40 @@ difference_cannot_overflow_p (struct ivopts_data
> *data, tree base, tree offset)
> }
> }
>
> +/* Return true if STEP * VAL, computed in OFF_TYPE, is known to be a
> + non-negative offset which does not overflow, i.e. if the value of VAL is
> + non-negative and multiplying it by STEP fits in the signed range of
> + OFF_TYPE. */
> +
> +static bool
> +nonneg_scaled_offset_p (tree val, HOST_WIDE_INT step, tree off_type)
> +{
> + if (!INTEGRAL_TYPE_P (TREE_TYPE (val)) || step == 0)
> + return false;
> +
> + signop sgn = TYPE_SIGN (TREE_TYPE (val));
> + int_range_max r;
> + if (!get_range_query (cfun)->range_of_expr (r, val)
> + || r.undefined_p ()
> + || wi::neg_p (r.lower_bound (), sgn))
> + return false;
> +
> + widest_int max = widest_int::from (r.upper_bound (), sgn);
> + widest_int limit
> + = widest_int::from (wi::max_value (TYPE_PRECISION (off_type), SIGNED),
> + SIGNED);
> + return wi::leu_p (max, wi::udiv_trunc (limit, absu_hwi (step)));
> +}
> +
> /* Tries to replace loop exit by one formulated in terms of a LT_EXPR
> comparison with CAND. NITER describes the number of iterations of
> - the loops. If successful, the comparison in COMP_P is altered
> accordingly.
> + the loops. If successful, the comparison in COMP_P is altered accordingly
> + and the bound in BOUND_P is recomputed.
>
> We aim to handle the following situation:
>
> sometype *base, *p;
> - int a, b, i;
> + unsigned a, b, i;
>
> i = a;
> p = p_0 = base + a;
> @@ -5265,20 +5292,32 @@ difference_cannot_overflow_p (struct ivopts_data
> *data, tree base, tree offset)
> }
> while (p < p_0 - a + b);
>
> - This preserves the correctness, since the pointer arithmetics does not
> - overflow. More precisely:
> + Note that the bound has to be computed as p_0 - a + b and not from the
> + number of iterations as p_0 + (b - a): the latter is only equivalent if
> + b - a does not wrap, which is not the case when the loop rolls zero times.
> +
> + For this to preserve correctness, we need to know that the values compared
> + in the transformed loop are ordered the same way as i and b are. Since
> the
> + comparison of the pointers is performed modulo the size of the address
> + space, this needs a + 1 > b to be an unsigned comparison, and the offsets
> + a and b scaled by the step of the candidate to be non-negative and to not
> + overflow. Then:
>
> 1) if a + 1 <= b, then p_0 - a + b is the final value of p, hence there
> is no
> - overflow in computing it or the values of p.
> - 2) if a + 1 > b, then we need to verify that the expression p_0 - a does
> not
> - overflow. To prove this, we use the fact that p_0 = base + a. */
> + overflow in computing it or the values of p, and the pointers increase
> + monotonically together with i.
> + 2) if a + 1 > b, then the loop exits at the first test, and b <= a implies
> + that p_0 - a + b lies between the valid addresses p_0 - a and p_0, so
> + the test indeed fails. Here we also need to verify that the expression
> + p_0 - a does not overflow, which we prove using p_0 = base + a. */
>
> static bool
> -iv_elimination_compare_lt (struct ivopts_data *data,
> +iv_elimination_compare_lt (struct ivopts_data *data, struct iv_use *use,
> struct iv_cand *cand, enum tree_code *comp_p,
> - class tree_niter_desc *niter)
> + class tree_niter_desc *niter, tree *bound_p)
> {
> - tree cand_type, a, b, mbz, nit_type = TREE_TYPE (niter->niter), offset;
> + tree cand_type, a, b, mbz, nit_type = TREE_TYPE (niter->niter);
> + tree off_type, offset, bound;
> class aff_tree nit, tmpa, tmpb;
> enum tree_code comp;
> HOST_WIDE_INT step;
> @@ -5303,6 +5342,11 @@ iv_elimination_compare_lt (struct ivopts_data *data,
> return false;
> step = int_cst_value (cand->iv->step);
>
> + /* The bound we compute below is the value the candidate has after the last
> + iteration, so the exit test has to see the incremented candidate. */
> + if (!stmt_after_increment (data->current_loop, cand, use->stmt))
> + return false;
> +
> /* Check that the number of iterations matches the expected pattern:
> a + 1 > b ? 0 : b - a - 1. */
> mbz = niter->may_be_zero;
> @@ -5346,14 +5390,25 @@ iv_elimination_compare_lt (struct ivopts_data *data,
> if (tmpb.n != 0 || maybe_ne (tmpb.offset, 1))
> return false;
>
> - /* Finally, check that CAND->IV->BASE - CAND->IV->STEP * A does not
> - overflow. */
> - offset = fold_build2 (MULT_EXPR, TREE_TYPE (cand->iv->step),
> - cand->iv->step,
> - fold_convert (TREE_TYPE (cand->iv->step), a));
> + /* The comparison A + 1 > B only tells us that B is at most A if it is
> + an unsigned one; otherwise B may well be negative. */
> + if (!TYPE_UNSIGNED (TREE_TYPE (a)))
> + return false;
> +
> + /* Check that CAND->IV->BASE - CAND->IV->STEP * A does not overflow. */
> + off_type = TREE_TYPE (cand->iv->step);
> + offset = fold_build2 (MULT_EXPR, off_type, cand->iv->step,
> + fold_convert (off_type, a));
> if (!difference_cannot_overflow_p (data, cand->iv->base, offset))
> return false;
>
> + /* The candidate is compared as an unsigned quantity, so the offsets by
> + which A and B move it away from CAND->IV->BASE - CAND->IV->STEP * A have
> + to be ordered the same way as A and B themselves. */
> + if (!nonneg_scaled_offset_p (a, step, off_type)
> + || !nonneg_scaled_offset_p (b, step, off_type))
> + return false;
> +
> /* Determine the new comparison operator. */
> comp = step < 0 ? GT_EXPR : LT_EXPR;
> if (*comp_p == NE_EXPR)
> @@ -5363,6 +5418,22 @@ iv_elimination_compare_lt (struct ivopts_data *data,
> else
> gcc_unreachable ();
>
> + /* Recompute the bound as CAND->IV->BASE - CAND->IV->STEP * A
> + + CAND->IV->STEP * B. Deriving it from the number of iterations, as
> + cand_value_at does, is not correct here: B - A is computed in NIT_TYPE
> + and converting it to OFF_TYPE is not value preserving when the loop
> + rolls zero times and B - A is thus negative. */
> + bound = fold_build2 (MINUS_EXPR, off_type,
> + fold_build2 (MULT_EXPR, off_type, cand->iv->step,
> + fold_convert (off_type, b)),
> + offset);
> + cand_type = TREE_TYPE (cand->iv->base);
> + if (POINTER_TYPE_P (cand_type))
> + *bound_p = fold_build_pointer_plus (cand->iv->base, bound);
> + else
> + *bound_p = fold_build2 (PLUS_EXPR, cand_type, cand->iv->base,
> + fold_convert (cand_type, bound));
> +
> return true;
> }
>
> @@ -5472,12 +5543,6 @@ may_eliminate_iv (struct ivopts_data *data,
> aff_combination_to_tree (&bnd));
> *comp = iv_elimination_compare (data, use);
>
> - /* It is unlikely that computing the number of iterations using division
> - would be more profitable than keeping the original induction variable.
> */
> - bool cond_overflow_p;
> - if (expression_expensive_p (*bound, &cond_overflow_p))
> - return false;
> -
> /* Sometimes, it is possible to handle the situation that the number of
> iterations may be zero unless additional assumptions by using <
> instead of != in the exit condition.
> @@ -5485,8 +5550,15 @@ may_eliminate_iv (struct ivopts_data *data,
> TODO: we could also calculate the value MAY_BE_ZERO ? 0 : NITER and
> base the exit condition on it. However, that is often too
> expensive. */
> - if (!integer_zerop (desc->may_be_zero))
> - return iv_elimination_compare_lt (data, cand, comp, desc);
> + if (!integer_zerop (desc->may_be_zero)
> + && !iv_elimination_compare_lt (data, use, cand, comp, desc, bound))
> + return false;
> +
> + /* It is unlikely that computing the number of iterations using division
> + would be more profitable than keeping the original induction variable.
> */
> + bool cond_overflow_p;
> + if (expression_expensive_p (*bound, &cond_overflow_p))
> + return false;
>
> return true;
> }
>
--
Richard Biener <[email protected]>
SUSE Software Solutions Germany GmbH,
Frankenstrasse 146, 90461 Nuernberg, Germany;
GF: Stefan Gaiser, Jochen Jaser, Abhinav Puri; (HRB 36809, AG Nuernberg)