Why can compiler not optimize out unused static std::string?
Asked Answered
B

1

7

If I compile this code with GCC or Clang and enable -O2 optimizations, I still get some global object initialization. Is it even possible for any code to reach these variables?

#include <string>
static const std::string s = "";

int main() { return 0; }

Compiler output:

main:
        xor     eax, eax
        ret
_GLOBAL__sub_I_main:
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:s
        mov     edi, OFFSET FLAT:_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1Ev
        mov     QWORD PTR s[rip], OFFSET FLAT:s+16
        mov     QWORD PTR s[rip+8], 0
        mov     BYTE PTR s[rip+16], 0
        jmp     __cxa_atexit

Specifically, I was not expecting the _GLOBAL__sub_I_main: section.

Godbolt link

Edit: Even with a simple custom defined type, the compiler still generates some code.

class Aloha
{
public:
    Aloha () : i(1) {}
    ~Aloha() = default;
private:
    int i;
};
static const Aloha a;
int main() { return 0; }

Compiler output:

main:
        xor     eax, eax
        ret
_GLOBAL__sub_I_main:
        ret
Borer answered 11/3, 2022 at 22:48 Comment(12)
std::string allows Small/Short string optimization. This normally means that inside the string is a union of a char[] and the member of string object. That could make the internals opaque to the optimizer so it might still initialize as there could be observable behavior that it doesn't want to accidently remove.Limpid
I was suspecting SSO and its variations might be the culprit but even if I have a custom type with nondefault constructor, compiler creates a label with ret instructionBorer
Both const and static should be redundant here. const already implies static in this case.Soule
In both examples, std::string and Aloha have constructors with side effects (initializing members), so the compiler is less likely to optimize them away.Jacobinism
My guess -- in the second case -- is that linker magic is intended to hook up all the static and global-variable initializations from all the modules. The ret corresponds to "all global variables in main.c have been initialized". I don't know why more code is generated for the unused std::string.Durra
@Soule interesting, didn't used to be that way. I have code that accesses non-static const global variables via extern and it works fine. Something must have changed in a newer version of the standard.Jacobinism
@RemyLebeau Shouldn't have. Thing is that if you used extern declaration in header included in both modules, it would affect the definition ... a little trick.Spectrochemistry
yeah, that was probably the case, thanksJacobinism
It it C++20? Before that ctor of string with that argument is not constexpr and in case of SSO it acquires address of object. In result - no optimization. Can be reproduced with a fundamental type just by using unary operator &Spectrochemistry
@Swift-FridayPie, thanks for the question, I didn't specify any extra flag for C++20, and used GCC 11.2. But even with -std=c++20 the result is the same. Can you give me concrete example with fundamental type, I think I didn't get the full picture ?Borer
@user17732522, I have tried also with const int&& i = 3; and static const int&& i = 3;. And static const was completely optimized out. Maybe I am missing a point but it seems in this case const doesn't completely imply static. Also if it is interesting, I may add the const int&& i = 3; case to the question as well?Borer
@Borer const is not top-level in the reference, so what I said doesn't apply to it.Soule
S
2

Compiling that code with short string optimization (SSO) may be an equivalent of taking address of std::string's member variable. Constructor have to analyze string length at compile time and choose if it can fit into internal storage of std::string object or it have to allocate memory dynamically but then find that it never was read so allocation code can be optimized out.

Lack of optimization in this case might be an optimization flaw limited to such simple outlying examples like this one:

const int i = 3;

int main()
{
    return (long long)(&i);  // to make sure that address was used
}

GCC generates code:

i:
        .long   3     ; this a variable
main:
        push    rbp
        mov     rbp, rsp
        mov     eax, OFFSET FLAT:i
        pop     rbp
        ret

GCC would not optimize this code as well:

const int i = 3;
const int *p = &i;
int main() {  return 0; }

Static variables declared in file scope, especially const-qualified ones can be optimized out per as-if rule unless their address was used, GCC does that only to const-qualified ones regardless of use case. Taking address of variable is an observable behaviour, because it can be passed somewhere. Logic which would trace that would be too complex to implement and would be of little practical value.

Of course, the code that doesn't use address

const int i = 3;
int main() { return i; }

results in optimizing out reserved storage:

main:
    mov     eax, 3
    ret

As of C++20 constexpr construction of std::string? Per older rules it could not be a compile-time expression if result was dependant on arguments. It possible that std::string would allocate memory dynamically if string is too long, which isn't a compile-time action. It appears that only mainstream compiler that supports C++20 features required for that it at this moment is MSVC in certain conditions.

Spectrochemistry answered 15/3, 2022 at 7:55 Comment(8)
GCC and clang do optimize away the storage for static int x = 2; if no functions in this compilation unit modify the value. See godbolt.org/z/77ra4oohW and compare what happens when you uncomment the other function. (i.e. they infer that it could be const, and optimize accordingly. It's not that expensive to track all writes to each variable as you compile, and then for each variable check if there are any writes. And maybe even what can be proved about its value-range, like non-negative.)Chap
That Godbolt link shows this applies even when there's a static int *px = &x; (which nothing reads), so I don't find this argument fully convincing. It's probably something along those lines, but the constructor or destructor must look like code that can access the reference, so gcc doesn't manage to see that there's no need for the reference or the pointed-to storage.Chap
@PeterCordes it's not about reading. It's about taking and using the address, not the value itself. Using address of a variable isn't a compile-time action, though taking a distance between locations in same object might be.. I tested on godbolt with -O3 and it doesn't optimize if an address was used by operator &. Investigation of further use of that address just doesn't happen while compiler formally isn't forbidden to do so.Spectrochemistry
How about the second example, I gave with a custom type? In this case, can it not optimize due to this pointer which carries the object address?Borer
You don't use address there neither you do use this, you only created an initialized storage in file scope which is optimizable. Aloha () : i(1) {} is equivalent of int i = 1; and it got a const qualifier while your program never had accessed that variable. As your custom isn't a constexpr, you didn't made optimization mandatory, but made it possible. Some compiler may decide that it cannot optimize if you write Aloha () { i = 1; } but GCC honors it as an equivalent.Spectrochemistry
Using the address of a static variable as an initializer for another is a compile-time / link-time action. If you want to distinguish that from use in a function, say so. godbolt.org/z/G41W1E6xo shows that clang and GCC are able to optimize away a static int *written_only; variable even if there's a non-static function that writes it (with the address of static int x;) But only clang also optimizes away the storage for x. (If the function doing that assignment is static and thus uncallable from anywhere, instead of global, then GCC can also optimize away everything)Chap
Your actual answer is two cases that are different from the string class case: the first is obviously harder (impossible) to optimize away, because the address escapes the compilation unit, which goes way beyond being used. The second is fairly trivial.Chap
Your update with a const int i = 3; ; const int *p = &i; example still doesn't seem exactly relevant. Both those variables are global not static, and thus accessible from outside this compilation unit. Of course they have to actually exist. GCC will optimize them both away if you use static const. The members (and member functions) of static const std::string s are not accessible outside the compilation unit, but GCC still doesn't optimize them away. So I assume it's something to do with how much complexity there is in the way the static constructor uses them.Chap

© 2022 - 2024 — McMap. All rights reserved.