Does `const T *restrict` guarantee the object pointed-to isn’t modified?
Asked Answered
I

4

10

Consider the following code:

void doesnt_modify(const int *);

int foo(int *n) {
    *n = 42;
    doesnt_modify(n);
    return *n;
}

where the definition of doesnt_modify isn’t visible for the compiler. Thus, it must assume, that doesnt_modify changes the object n points to and must read *n before the return (the last line cannot be replaced by return 42;).

Assume, doesnt_modify doesn’t modify *n. I thought about the following to allow the optimization:

int foo_r(int *n) {
    *n = 42;
    { /* New scope is important, I think. */
        const int *restrict n_restr = n;
        doesnt_modify(n_restr);
        return *n_restr;
    }
}

This has the drawback that the caller of doesnt_modify has to tell the compiler *n isn’t modified, rather than that the function itself could tell the compiler via its prototype. Simply restrict-qualifying the parameter to doesnt_modify in the declaration doesn’t suffice, cf. “Is top-level volatile or restrict significant [...]?”.

When compiling with gcc -std=c99 -O3 -S (or Clang with the same options), all functions are compiled to equivalent assembly, all re-reading the 42 from *n.

  1. Would a compiler be allowed to do this optimization (replace the last line by return 42;) for foo_r? If not, is there a (portable, if possible) way to tell the compiler doesnt_modify doesn’t modify what its argument points to? Is there a way compilers do understand and make use of?

  2. Does any function have UB (provided doesnt_modify doesn’t modify its argument’s pointee)?

Why I think, restrict could help here (From C11 (n1570) 6.7.3.1 “Formal definition of restrict”, p4 [emph. mine]):

[In this case, B is the inner block of foo_r, P is n_restr, T is const int, and X is the object denoted by *n, I think.]

During each execution of B, let L be any lvalue that has &L based on P. If L is used to access the value of the object X that it designates, and X is also modified (by any means), then the following requirements apply: T shall not be const-qualified. […]

$ clang --version
Ubuntu clang version 3.5.0-4ubuntu2 (tags/RELEASE_350/final) (based on LLVM 3.5.0)
Target: x86_64-pc-linux-gnu

Gcc version is 4.9.2, on an x86 32bit target.

Ivon answered 13/10, 2014 at 18:14 Comment(3)
It's legal for doesnt_modify2 to cast away the constness and modify the object pointed to, as long as the object itself wasn't declared const.Hennahane
@Hennahane Yes, that's the point. The question is, if restrict makes any difference here.Ivon
Interesting - you're promising that nothing else aliases it, and that this particular pointer can't modify it. I'm very curious to see what this means.Judaic
O
9

Version 1 seems clearly specified by the formal definition of restrict (C11 6.7.3.1). For the following code:

const int *restrict P = n;
doesnt_modify(P);
return *P;

the symbols used in 6.7.3.1 are:

  • B - that block of code
  • P - the variable P
  • T - the type of *P which is const int
  • X - the (non-const) int being pointed to by P
  • L - the lvalue *P is what we're interested in

6.7.3.1/4 (partial):

During each execution of B, let L be any lvalue that has &L based on P. If L is used to access the value of the object X that it designates, and X is also modified (by any means), then the following requirements apply: T shall not be const-qualified [...] If these requirements are not met, then the behavior is undefined.

Note that T is const-qualified. Therefore, if X is modified in any way during this block (which includes during the call to a function in that block), the behaviour is undefined.

Therefore the compiler can optimize as if doesnt_modify did not modify X.


Version 2 is a bit more difficult for the compiler. 6.7.6.3/15 says that top-level qualifiers are not considered in prototype compatibility -- although they aren't ignored completely.

So although the prototype says:

void doesnt_modify2(const int *restrict p);

it could still be that the body of the function is declared as void doesnt_modify2(const int *p) and therefore might modify *p.

My conclusion is that if and only if the compiler can see the definition for doesnt_modify2 and confirm that p is declared restrict in the definition's parameter list then it would be able to perform the optimization.

Onym answered 1/3, 2015 at 23:3 Comment(12)
I might be wrong, but I think const can legally be cast away. The resulting pointer would still be "based on" the original pointer and so would be unconstrained by restrict. The only case in which this would provoke UB would be if the original (pointed-to) object was declared const.Vange
Strike that. I had missed the significance of "T shall not be const-qualified" in 6.7.3.1.Vange
Minor: 6.7.6.3/13 is about storage-class specifiers (the only one allowed in function arguments is register), not about qualifiers, and thus doesn't apply (though I agree with the point made, top-level qualifiers are usually ignored here, it's not clear to me where the standard mandates this, but it's at least common interpretation).Ivon
Have started a separate question specifically for the prototype case, although I can't see any other interpretation at this stageOnym
@MattMcNabb: Thanks, I'm currently thinking about editing that part out of my question and link to yours instead. As I currently see it, an inline wrapper would do: inline void doesnt_modify(const int *restrict n) { real_doesnt_modify(n); } just to show the compiler some definition. Do you think, I should include this into my question instead? (And do you think the inline definition would suffice?)Ivon
@Ivon I'm not sure if the inline wrapper helps or not, since the compiler doesn't know about real_doesnt_modify . The question seems simpler without it.Onym
My idea with the inline wrapper was to show the compiler some definition where the restrict qualifier is kept. (If the top-level restrict from the declaration is simply removed, we don't have any restrict left without the inline wrapper.) It would have the advantage that it can be expressed by the called function not to modify its argument, rather than by the caller (where it must be repeated every time and where it actually doesn't belong from a design point-of-view).Ivon
I'd like to add one exception, that of T being volatile. The C standard (6.7.3 Type Qualifiers, clause 11) has a curious example of an extern const volatile int real_time_clock; which, albeight const, can mutate its value because it is a device register, but it's only read-only from the perspective of the C language; The value is changed by hardware.Darin
Is there any requirement that if a function's prototype declares a parameter const * restrict, the actual definition must abide by the limitations implied thereby whether or not it declares the pointer likewise?Wystand
@Wystand no, only the presence in the definition is significantOnym
@M.M: It's too bad there aren't any qualifiers via which a function can promise to refrain from modifying and/or persisting a pointer that is given to it, since such information could be very useful to an optimizer, and the requirement that a const * restrict not be modified within area where the definition is visible isn't nearly as useful as knowing--when generating outside code--that it won't be modified within the function.Wystand
Yeah, perhaps restrict should be a part of the signature. I haven't thought it through to see if there are associated problems.Onym
K
1

Generally, restrict means that the pointer is not aliased (i.e. only it or a pointer derived from it can be used to access the pointed-to object).

With const, this means that the pointed-to object cannot be modified by well-formed code.

There is, however, nothing to stop the programmer breaking the rules using an explicit type conversion to remove the constness. Then the compiler (having been beaten into submission by the programmer) will permit an attempt to modify the pointed-to object without any complaint. This, strictly speaking, results in undefined behaviour so any result imaginable is then permitted including - possibly - modifying the pointed-to object.

Kurt answered 2/3, 2015 at 9:35 Comment(0)
B
1

If not, is there a (portable, if possible) way to tell the compiler doesnt_modify doesn’t modify what its argument points to?

No such way.

Compiler optimizers have difficulty optimizing when pointer and reference function parameters are involved. Because the implementation of that function can cast away constness compilers assume that T const* is as bad as T*.

Hence, in your example, after the call doesnt_modify(n) it must reload *n from memory.

See 2013 Keynote: Chandler Carruth: Optimizing the Emergent Structures of C++. It applies to C as well.

Adding restrict keyword here does not change the above.

Befog answered 7/3, 2015 at 15:23 Comment(0)
W
0

Simultaneous use of a restrict qualifier on a pointer-type parameter and a const qualifier on its target type would invite a compiler to assume that no region of storage which is accessed during the lifetime of the pointer object via the pointer contained therein or any pointer derived from it, will be modified via any means during that pointer's lifetime. It generally says nothing whatsoever about regions of storage which are not accessed using the pointer in question.

The only situations where const restrict would have implications for an entire object would be those where pointer is declared using array syntax with a static bound. In that situation, behavior would only be defined in cases where the entire array object could be read (without invoking UB). Since reading any part of the array object which changes during function execution would invoke UB, code would be allowed to assume that no portion of the array can be changed in any fashion whatsoever.

Unfortunately, while a compiler that knew that a function's actual definition starts with:

void foo(int const thing[restrict static 1]);

would be entitled to assume that no part of *thing would be changed during the function's execution, even if the object might be one the function could otherwise access via pointer not derived from thing, the fact that a function's prototype includes such qualifiers would not compel its definition to do likewise.

Wystand answered 29/8, 2018 at 16:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.