2

As a follow up to my previous question, consider the following code:

int f(int *p);

static inline int g0(int *p) {
  *p=0;
  f(NULL); // might modify *p
  return *p==0;
}
int caller0(int *q) { return g0(q); }

Above, after calling f, the compiler has to fetch *p and perform the comparison to 0, because p might alias memory that is also available to f. However:

static inline int g1(int *restrict p) {
  *p=0;
  f(NULL); // cannot modify *p
  return *p==0;
}
int caller1(int *q) { return g1(q); }

Because of restrict, the compiler can deduce that it is illegal for f to modify *p, thus it can just return 1, optimizing out the fetch and the comparison. Clang 17 does this optimization. However:

int caller2(int *q) {
  { int *restrict p=q;
    *p=0;
    f(NULL);
    return *p==0;
  }
}

This caller2 seems to me to be equivalent to caller1, just inlined by hand. The same Clang compiler did not optimize this (it emits the fetch and the comparison).

First question: Is the compiler allowed to optimize caller2?

Further:

int caller3(int *q) {
  ... // many lines of code
  { int *restrict p=q;
    *p=0;
    ... // do something useful
    f(q);
    if (*p==0) { ... } // do something useful
  }
  ... // many lines of code
}

Now this is the part I actually care about. If the compiler is allowed to optimize caller2, is it allowed to optimize caller3?

According to my reading of the standard, q is not "based on" p here, and *p is modified through the restrict-qualified pointer, therefore the compiler can assume that f does not modify *p (further; f isn't even allowed to read *q, so the compiler is even allowed to reorder the assignment *p=0 after the call to f (though f may legally read/write *(q+1))).

Is this correct? If it is, it suggests that restrict can be creatively used even inside the bodies of your functions to make various hints about aliasing to the compiler.

3
  • Yes, I think this does work. Even just int dummy = *p; or (void)*p would make it UB for anything in any callee to modify *p via a differently-derived pointer. And I agree that access via the original q or an arg copy of it probably doesn't count as "based on" int *restrict p = q. But that's the part I'm least sure about, only maybe 90 to 95% sure. Commented Jan 10, 2024 at 18:02
  • 2
    Compilers not optimizing when restrict is introduced inside a function as with int * restrict p = q; is a deficiency discussed in a recent question, marked as a duplicate of this one. Commented Jan 10, 2024 at 21:45
  • Oh I see. So this question has no hope of being practically useful anytime soon. A standard feature that's never implemented for valid reasons is not worth worrying about then, I guess. Will stick to function parameters. Commented Jan 11, 2024 at 0:16

1 Answer 1

0

Because of restrict, the compiler can deduce that it is illegal for f to modify *p, thus it can just return 1, optimizing out the fetch and the comparison.

"Illegal" is not the correct term. The spec does not forbid such behavior. It simply washes its hands of the situation if f does modify *p. In that case, that particular call to g1(), and therefore the whole execution of the program, has undefined behavior. Thus (the modern interpretation goes) the compiler can produce a program that acts as if f does not modify *p, regardless of actual fact.

Is the compiler allowed to optimize caller2?

If we accept that the compiler is allowed to optimize caller1() as described, then yes, it is allowed to perform the analogous optimization on caller2(). But it is not obligated to do so.

If the compiler is allowed to optimize caller2, is it allowed to optimize caller3?

Yes, if we accept the argument allowing the optimization of caller2 as described, then the same argument allows the analogous optimization of caller3.

As you observe, q is not "based on" p. Since p has no linkage and neither its address nor the address of another object whose value is based on p is ever taken, the compiler can conclude that f() has no access to any pointer based on restrict-qualified p, and therefore that the behavior of the program is undefined if evaluation of f(q) modifies *p.

If it is, it suggests that restrict can be creatively used even inside of your functions to make various hints about aliasing to the compiler.

I guess you could characterize it that way. I'd inclined to say something more like "restrict can be used inside functions to invite the compiler to emit broken code for them".

In the example case, if you wanted to enable the compiler to optimize based on the assumption that f will not modify the data to which its argument points then it would be cleaner and clearer to declare its argument as a pointer to const data:

int f(const int *p);

If you can't do that, then the non-aliasing assumption you want to permit probably is not safe.

Overall, restrict doesn't really make sense for use on automatic variables. The compiler does not need your help to see the aliasing opportunities involving those, and suggesting that it make decisions based on a more limited view of those is asking for trouble.

Sign up to request clarification or add additional context in comments.

5 Comments

This question was asked in the context of another question (linked), which establishes an interesting point about restrict. If you follow the discussion there, you'll see that (1) const can be cast away so the compiler cannot use that as assumption what f will modify; (2) f can indeed access q without UB, just not *q (it might access q[1]). As you already know from still another of my questions, I am not convinced that the standard formally defines restrict in an unambiguous way. Trying to document the corner cases with these follow up questions, for me and for other readers.
@log65536, Re (1): I acknowledge that it is possible -- albeit risky and inviting UB from a different direction -- to cast away const. Re (2): I never said anything about f not accessing q. Overall: the main takeaway from this answer is in the concluding paragraph: restrict doesn't really make sense for use on automatic variables. If you're not satisfied that its behavior is well defined by the spec, or at least that the intention is well understood by implementors, then that's all the more reason not to use it in that (or any) way.
(1) Casting away const is not UB, if the object itself is not read-only. Bad practice or not, the compiler is forbidden from using that information for optimization, because that would be generating incorrect code according to the standard. (2) The function f might be some trivial library function that (to a human) is absolutely clear it won't screw you up. I'm exploring whether such use of restrict can communicate that effectively to the compiler.
(3) Yes, I'm not convinced that the spec defines restrict fully (though it's OK as far as this question is concerned). But I do think compilers most of the time get it right, because they use a different abstract model, more close to the machine, so to speak. Though there have been bugs, e.g. open-std.org/jtc1/sc22/wg14/www/docs/n2260.pdf. I also think that, if used right, restrict does enable useful optimizations. There is a need for such a mechanism, if restrict can't be rescued, maybe something else, but the standard is evolving in that direction.
Re “… if you wanted to enable the compiler to optimize based on the assumption that f will not modify the data to which its argument points then it would be cleaner and clearer to declare its argument as a pointer to const data…”: Declaring a parameter as a pointer to const data does not enable any optimization. Outside of a definition, const only serves as an advisory qualifier, requiring the compiler to generate diagnostic messages when used in violations of constraints. Functions f(const T *p) and f(T *p) may use p in all the same ways after casting to T *.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.