Is this "elision failure" language-mandated?

274 views Asked by At

Consider the following code:

#include <utility>
#include <string>

int bar() {
    std::pair<int, std::string> p { 
        123, "Hey... no small-string optimization for me please!" };
    return p.first;
}

(simplified thanks to @Jarod42 :-) ...)

I expect the function to be implemented as simply:

bar():   
        mov eax, 123
        ret

but instead, the implementation calls operator new(), constructs an std::string with my literal, then calls operator delete(). At least - that's what gcc 9 and clang 9 do (GodBolt). Here's the clang output:

bar():                                # @bar()
        push    rbx
        sub     rsp, 48
        mov     dword ptr [rsp + 8], 123
        lea     rax, [rsp + 32]
        mov     qword ptr [rsp + 16], rax
        mov     edi, 51
        call    operator new(unsigned long)
        mov     qword ptr [rsp + 16], rax
        mov     qword ptr [rsp + 32], 50
        movups  xmm0, xmmword ptr [rip + .L.str]
        movups  xmmword ptr [rax], xmm0
        movups  xmm0, xmmword ptr [rip + .L.str+16]
        movups  xmmword ptr [rax + 16], xmm0
        movups  xmm0, xmmword ptr [rip + .L.str+32]
        movups  xmmword ptr [rax + 32], xmm0
        mov     word ptr [rax + 48], 8549
        mov     qword ptr [rsp + 24], 50
        mov     byte ptr [rax + 50], 0
        mov     ebx, dword ptr [rsp + 8]
        mov     rdi, rax
        call    operator delete(void*)
        mov     eax, ebx
        add     rsp, 48
        pop     rbx
        ret
.L.str:
        .asciz  "Hey... no small-string optimization for me please!"

My question is: Clearly, the compiler has full knowledge of everything going on inside bar(). Why is it not "eliding"/optimizing the string away? More specifically:

  1. At the basic level there's the code between then new() and delete(), which AFAICT the compiler knows results in nothing useful.
  2. Secondarily, the new() and delete() calls themselves. After all, small-string-optimization is allowed by the standard AFAIK, so even though clang/gcc hasn't chosen to use that - it could have; meaning that it's not actually required to call new() or delete() there.

I'm particularly interested in what part of this is directly due to the language standard, and what part is compiler non-optimality.

3

There are 3 answers

0
einpoklum On

Following the discussion in various answers and comments here, I have now filed the following bugs against GCC and LLVM regarding this issue:

  1. GCC bug 94293: [missed optimization] new+delete of unused local string not removed

    Minimal testcase (GodBolt):

    void foo() {
        int *p = new int[1];
        *p = 42;
        delete[] p;
    }
    
  2. GCC bug 94294: [missed optimization] Useless statements populating local string not removed

    Minimal testcase (GodBolt):

    void foo() {
        std::string s { "This is not a small string" };
    }
    
  3. LLVM bug 45287: [missed optimization] failure to drop unused libstdc++ std::string.

    Minimal testcase (GodBolt):

    void foo() {
        std::string s { "This is not a small string" };
    }
    

Thanks goes to: @JeffGarret, @NicolBolas, @Jarod42, Marc Glisse .

Update, August 2021: With recent versions of clang++, g++ and libstc++, all of these minimal testcases eschew memory allocation as one would expect. clang++ also has this behavior for OP's program in the question, but GCC still allocates and deallocates.

15
Nicol Bolas On

Nothing in your code represents "elision" as that term is commonly used in a C++ context. The compiler is not permitted to remove anything from that code on the grounds of "elision".

The only grounds a compiler has to remove the creation of that string is on the basis of the "as if" rule. That is, is the behavior of the string creation/destruction visible to the user and therefore not able to be removed?

Since it uses std::allocator and the standard character traits, the basic_string construction and destruction itself is not being overridden by the user. So there is some basis for the idea that the string's creation is not a visible side-effect of the function call and thus could be removed under the "as if" rule.

However, because std::allocator::allocate is specified to call ::operator new, and operator new is globally replaceable, it is reasonable to argue that this is a visible side effect of the construction of such a string. And therefore, the compiler cannot remove it under the "as if" rule.

If the compiler knows that you have not replaced operator new, then it can in theory optimize the string away.

That doesn't mean that any particular compiler will do so.

5
Jeff Garrett On

The question is can the program

int bar() {
    std::pair<int, std::string> p { 
        123, "Hey... no small-string optimization for me please!" };
    return p.first;
}

be validly be optimized to

int bar() {
    return 123;
}

tldr, yes, I think.

And clang does with libc++: godbolt

About std::string, the standard says string.require/3

Every object of type basic_­string uses an object of type Allocator to allocate and free storage for the contained charT objects as needed.

"as needed". std::string is allowed to decide when to use the allocator (which is I believe the justification for SSO being valid). Its member functions do not mandate an allocation. Therefore the allocation may be elided.