2

Take this toy code (godbolt link):

int somefunc(const int&);
void nothing();

int f(int i) {
    i = somefunc(i);
    i++;
    nothing();
    i++;
    nothing();
    i++;
    return i;
}

As can be seen in the disassembly at the link, the compiler reloads i from the stack 3 times, increments and stores back.

If somefunc is modified to accept int by value, this doesn't happen.

(1) Is the optimizer 'afraid' that since somefunc has access to is address, it can indirectly modify it? Can you give an example of well defined code that does that? (remember that const_cast'ing away and modifying is undefined behavior).

(2) Even if that was true, I'd expect that decorating somefunc with __attribute__((pure)) would stop that pessimization. It doesn't. Why?

Are these llvm missed optimizations?


Edit: If somefunc returns void, __attribute__((pure)) does kick in as expected:

void somefunc(const int&) __attribute__((pure));
void nothing();

int f(int i) {
    somefunc(i);
    i++;
    nothing();
    i++;
    nothing();
    i++;
    return i;
}

Maybe this attribute is sort of half baked (it is rare in practice).

Ofek Shilon
  • 14,734
  • 5
  • 67
  • 101
  • 8
    `const_cast'ing away` is actually [allowed](https://stackoverflow.com/a/19554871/4074081) if the original object is not defined as const – dewaffled Jan 09 '22 at 18:52
  • @dewaffled inside `somefunc` the argument is considered const – Ofek Shilon Jan 09 '22 at 18:55
  • 3
    if you define `somefunc` inline so compiler sees it does not `const_cast` then it will be optimized. the language allows `const_cast` if the original object is not const (it is not in your example). if it was not allowed, const_cast just would not be in the language. – dewaffled Jan 09 '22 at 19:00
  • I didn't inline somefunc – Ofek Shilon Jan 09 '22 at 19:06
  • 5
    _"(remember that const_cast'ing away and modifying is undefined behavior)"_ this seems to be the basis of your question, and it is not correct. `const_cast` exists in the language because there are valid uses, and it could be used here. – Drew Dormann Jan 09 '22 at 19:08
  • Change the line to `i = somefunc(i+0);` and the optimizer should be happy again. – Eljay Jan 09 '22 at 19:32
  • @Eljay we actually did something like `+i` .. – Ofek Shilon Jan 09 '22 at 19:33
  • 1
    "escape analysis": `i` escapes through `somefunc` (which may store its address in a global variable), and may thus be used (read/write) by `nothing`. The attribute does seem to help with gcc, maybe you could report the missed optimization to llvm? – Marc Glisse Jan 09 '22 at 20:00
  • 3
    @MarcGlisse thanks, I just did: https://github.com/llvm/llvm-project/issues/53102 – Ofek Shilon Jan 09 '22 at 20:15
  • 1
    A pure function that returns void is a NOP, and indeed the compiler eliminates its call, so I don't think that says much. – Marc Glisse Jan 09 '22 at 20:21
  • if `somefunc()` has a global state - e.g. saves a pointer to its argument into a global variable and `nothing()` is using this pointer. [example](https://godbolt.org/z/cbPh6MKqn) . In this case, compilers cannot optimize increment. – Alexander Jan 09 '22 at 20:24
  • @Alexander If the function is declared `pure`, it’s not allowed to do that. – Sneftel Jan 09 '22 at 20:47
  • @Sneftel my comment was about (1) – Alexander Jan 10 '22 at 06:44

1 Answers1

6

As mentioned in a comment, using const_cast to remove the constness of a reference to an object that was defined as non-const is well-defined. In fact, that's its only real use.

As for __attribute__((pure)): Who knows. The nothing() calls are necessary to reproduce the situation; if those are marked pure then proper optimization is done; the invocation of somefunc doesn't have much of an impact on that situation. Basically, the compiler tends to be quite conservative unless everything in a code block is pure. While it should arguably be able to deduce that nothing() doesn't affect i, that's very much a "best effort" area of optimization and not something that properly optimized code should rely on.

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • The implementation of `somefunc`, in a different translation unit, cannot know how the int passed as argument was originally defined. How can such an implementation const_cast away and still have well-defined behaviour? – Ofek Shilon Jan 09 '22 at 19:25
  • 5
    @OfekShilon it would not be the responsibility of the implementation to _know_ whether it has well-defined behavior. That would be the developer's responsibility. – Drew Dormann Jan 09 '22 at 19:30
  • 3
    The programmer has to make sure. E.g. by code documentation. If it is originally defined as `non-const`, then the behaviour is well-defined. `const_cast` is an advanced (=dangerous, if you are not careful) tool. – Sebastian Jan 09 '22 at 19:30
  • 3
    @OfekShilon: The fact that you're using `const_cast` to cast away const and modify the pointed-to object lets the compiler assume that original object was non-`const`. Otherwise it would be UB, and literally anything is allowed to happen in that case, so the optimizer can just assume that UB didn't happen. (i.e. make code that works for the valid case, without caring about what happens in the invalid case.) This is basically the point of declaring things UB, rather than errors an implementation is required to detect and print an error message for. (`clang -fsanitize=undefined` *might* do so – Peter Cordes Jan 09 '22 at 20:31
  • @OfekShilon: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html – Peter Cordes Jan 09 '22 at 20:34