On Fri, 21 Mar 2025, Richard Biener wrote:

Issues
------
The semantics I've described above result in many reports of
miscompilations that I haven't reported yet.

As mentioned earlier, the vectorizer can use POINTER_PLUS_EXPR to generate
pointers that extend up to a vector length past the object (and similarly,
up to one vector length before the object in a backwards loop). This is
invalid if the object is too close to address 0 or 0xffffffffffffffff.

And this out of bounds distance can be made arbitrarily large, as can be
seen by compiling the following function below for x86_64 with -O3:

void foo(int *p, uint64_t s, int64_t n)
{
   for (int64_t i = 0; i < n; i++)
     {
       int *q = p + i * s;
       for (int j = 0; j < 4; j++)
        *q++ = j;
     }
}

Here, the vectorized code add s * 4 to the pointer ivtmp_8 at the end of
each iteration:

   <bb 5> :
   bnd.8_38 = (unsigned long) n_10(D);
   _21 = s_11(D) * 4;

   <bb 3> :
   # ivtmp_8 = PHI <ivtmp_7(6), p_12(D)(5)>
   # ivtmp_26 = PHI <ivtmp_20(6), 0(5)>
   MEM <vector(4) int> [(int *)ivtmp_8] = { 0, 1, 2, 3 };
   ivtmp_7 = ivtmp_8 + _21;
   ivtmp_20 = ivtmp_26 + 1;
   if (ivtmp_20 < bnd.8_38)
     goto <bb 6>;
   else
     goto <bb 7>;

   <bb 6> :
   goto <bb 3>;

This means calling foo with a sufficiently large s can guarantee wrapping
or evaluating to 0, even though the original IR before optimization did
not wrap or evaluating to 0. For example, when calling foo as:

   foo(p, -((uintptr_t)p / 4), 1);

I guess this is essentially the same issue as PR113590, and could be fixed
by moving all induction variable updates to the latch.

Yes.

But if that
happens, the motivating examples for needing to handle invalid pointer
values would no longer occur. Would that mean smtgcc should adopt a more
restrictive semantics for pointer values?

In principle yes, but PR113590 blocks this I guess.  Could smtgcc consider
a SSA def to be happening only at the latest possible point, that is,
"virtually"
sink it to the latch?  (I realize when you have two dependent defs that can
be moved together only such "virtual" sinking could be somewhat complicated)

I think it feels a bit strange (and brittle) to define the semantics by
virtually sinking everything as much as possible. But isn't this sinking
just another way of saying that the operation is UB only if the value is
used? That is, we can solve this essentially the same way LLVM does with
its deferred UB.

Yeah, that's another way of thinking.  Though technically GCC doesn't implement
defered UB - for the case above we're simply lucky nothing exploits the UB
that is in principle there.

The idea is that POINTER_DIFF_EXPR, PLUS_EXPR, etc. are defined for all
inputs, but the result is a poison value when it doesn't fit in the return
type. Any use of a poison value is UB.

This is trivial to implement in smtgcc and would solve PR113590.

I _think_ it's a reasonable thing for smtgcc to do.  For GCC itself it would
complicate what is UB and what not quite a bit - what's the ultimate "use"
UB triggers?  I suppose POISON + 1 is simply POISON again.  Can
you store POISON to memory or is that then UB at the point of the store?
Can you pass it to a call?  What if the call is inlined and just does
return arg + 1?  IIRC I read some paper about this defered UB in LLVM
and wasn't convinced they solve any real problem.

I agree deferred UB causes more problems than it solves... And I think it's important that smtgcc uses the same semantics as GCC, so I'll keep the current behavior.

   /Krister

Reply via email to