7

I have already searched for some answers on Google and Stack Overflow, and I am aware that compilers cannot assume that functions won't modify arguments passed through const references, as these functions might obtain a non-const reference via const_cast. However, doing this is undefined behavior when the original object itself is defined as const. From cppreference

Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

For the following code

void fun(const int&);

int f1() {
    const int i = 3;
    fun(i);
    return i;
}

static int bar(const int i) {
    fun(i);
    return i;
}

int f2() {
    return bar(3);
}

Both GCC and Clang are capable of optimizing the function f1() to directly return 3, as the compilers consider that calling fun(i) won't modify the value of i, since such an action would result in undefined behavior. However, both GCC and Clang are unable to apply the same optimization to the function f2(). The compilers still generate code to load the value of i from memory. Below is the code for f1() and f2() generated by GCC. Compiler Explorer

f1():
        subq    $24, %rsp
        leaq    12(%rsp), %rdi
        movl    $3, 12(%rsp)
        call    fun(int const&)
        movl    $3, %eax  ! <-- Returns 3 directly.
        addq    $24, %rsp
        ret
f2():
        subq    $24, %rsp
        leaq    12(%rsp), %rdi
        movl    $3, 12(%rsp)
        call    fun(int const&)
        movl    12(%rsp), %eax ! <-- Load the return value from memory.
        addq    $24, %rsp
        ret

Even though the standard does not require that compilers must perform such optimizations, I believe compilers should have the capability to optimize f2() to directly return 3 as well. In my view, this would result in more efficient code (please correct me if I'm mistaken). When the compiler inlines the calling bar(3) into function f2(), it should be able to deduce that calling fun(i) will not modify the value of i.

Continuing with another example. When I replace the variable i in function f1() with a class type, Clang is still able to optimize it to return 3. However, GCC opts to load the return value from memory instead:

struct A {
    int i;
};

void fun(const A&);

int f3() {
    const A a{3};
    fun(a);
    return a.i;
}

Compiler Explorer

Here is the code generated by GCC:

f3():
        subq    $24, %rsp
        leaq    12(%rsp), %rdi
        movl    $3, 12(%rsp)
        call    fun(A const&)
        movl    12(%rsp), %eax  ! <-- Load the return value from memory.
        addq    $24, %rsp
        ret

and Clang:

f3():
        pushq   %rax
        movl    $3, (%rsp)
        movq    %rsp, %rdi
        callq   fun(A const&)@PLT
        movl    $3, %eax  ! <-- Returns 3 directly.
        popq    %rcx
        retq

Why doesn't GCC optimize the function to directly return 3? Is it because GCC considers loading the return value from memory to be equally efficient as directly returning a constant?

Boann
  • 48,794
  • 16
  • 117
  • 146
Pluto
  • 910
  • 3
  • 11
  • 6
    I'm not a compiler writer, but it may just be a missed optimization path. To paraphrase Chandler Carruth, sometimes compilers just run out of smarts. – Stephen Newell Aug 27 '23 at 15:07
  • 4
    GCC and Clang are both happy to optimize [in the case that they know for sure that `fun` doesn't do anything funny](https://godbolt.org/z/rEWvPeKjq), that seems like evidence that they're being conservative regarding something `fun` might do. Even though it "can't". – harold Aug 27 '23 at 15:13
  • Related: [Why don't c++ compilers replace this access to a const class member with its value known at compile time?](https://stackoverflow.com/q/70522927) - but in that case (with a `const` struct member), there are non-UB ways to generate an object with a different value. Here there isn't; the underlying object is `const`, and a reload could only matter if the function changed it. I thought there was an existing Q&A about missed optimizations where compilers don't assume that `const` locals keep their value, but I haven't found it. This looks like a GCC missed-optimization. – Peter Cordes Aug 27 '23 at 19:00
  • 1
    Generally, `const` doesn't mean it can't change, it means *you* can't change it, and that differs between C & C++ and by standard version. However, here I don't see how it might change by some other way, seems like a closed case but not handled for some reason... – Erik Eidt Aug 27 '23 at 21:56
  • 2
    @ErikEidt: That's what a reference-to- `const` or pointer-to-`const` means. An object created `const` cannot change its value during its lifetime. C++ formerly forbade fiddling with lifetime of `const` objects while they were being used, but due to a DR that workaround is now allowed. (Being a DR made the change retroactive; there is now no standard version which prevents changing the value of a `const` object via lifetime fiddling, breaking existing compliant code :(.) – Ben Voigt Aug 28 '23 at 14:19
  • what did the gcc folks say when you asked? – old_timer Aug 29 '23 at 01:46
  • 1
    @BenVoigt: Which DR do you have in mind for lifetime of `const` object? Are you talking about things that might be sub-objects of a class, and placement-`new`? (the C++20 change: https://github.com/cplusplus/draft/commit/fd8ff6441f93024bd0ee6e03a03c08be8e1b5ce0). In this case, the compiler knows the complete object is a `const int i` which it chose to put on the stack. Are you saying that `fun(&i)` could make a new object in that space, and the later `return i;` should load that new object via the old name? Or is it still just a missed opt but GCC is conservative because of other cases? – Peter Cordes Aug 29 '23 at 03:36
  • 2
    @PeterCordes I think since `i` is a complete `const` object, even if `fun(&i)` uses placement new to create a new object on the storage occupied by `i`, the original name `i` cannot automatically refer to the new object, because `i` is not transparently replaceable by the new object. The compiler can take advantage of this by assuming that the value of `i` will not change unless `std::launder` is used. – Pluto Aug 29 '23 at 04:03
  • @Pluto: Yeah, I don't think that would be valid either since it's a complete object where the compiler can see the declaration, not just via a pointer or reference. I hope ISO C++ doesn't allow anything to change the bytes in that storage because that would be silly. My suggestion was the only thing could think of for possible lifetime shenanigans, not something I actually thought would be legal. – Peter Cordes Aug 29 '23 at 05:48

0 Answers0