这个问题是在询问“省略失效”是否是语言规定所导致的。

7

考虑以下代码:

#include <utility>
#include <string>

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

我期望这个函数的实现应该尽可能地简单:
bar():   
        mov eax, 123
        ret

但是实现调用的是 operator new(), 使用我的字面量构造一个 std::string, 然后调用 operator delete(). 至少 - 这是 gcc 9 和 clang 9 的做法 (GodBolt). 这是 clang 输出的结果:

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!"

我的问题是:编译器显然对 bar() 内部发生的一切都有充分的了解。为什么它不会 "省略"/优化字符串?更具体地说:
  1. 基本上是在 new()delete() 之间的代码,我认为编译器知道这些代码没有任何有用的结果。
  2. 其次,new()delete() 的调用本身。毕竟,标准允许使用小字符串优化(SSO),所以即使clang/gcc没有选择使用它,但它 可以 使用;这意味着在那里并不真正需要调用 new()delete()
我特别想知道这个问题中哪一部分是直接由语言标准引起的,哪一部分是编译器非最优化产生的。

1
“new”/“delete”不是可观察行为吗?(而且“new”可能会抛出异常) - Jarod42
  1. 我不确定在C++20中是否可以有constexpr向量。
  2. 即使是这样 - 如果理论上允许使用小字符串优化来处理最多1,000,000个字符,那么就意味着你可以为foo()和因此为bar()允许不同的可观察行为。
- einpoklum
1
根据 https://en.cppreference.com/w/cpp/language/new#Allocation,自 C++14 起才能省略分配。 - Jarod42
编译器优化未使用的字符串会导致不一致行为,这在delete new int;中也存在问题。您可以通过使用-fno-delete-null-pointer-checks标志来抑制该优化并解决此问题。 - Jarod42
我认为SSO应该是一致的,否则它主要是ODR违规。 - Jarod42
显示剩余4条评论
3个回答

4

你的代码中没有任何内容代表"省略",在C++上下文中通常使用这个术语。编译器不允许基于"省略"的理由从该代码中删除任何内容。

编译器唯一可以删除该字符串创建的依据是基于"好像"规则。也就是说,字符串的创建/销毁行为是否对用户可见,因此无法被删除?

由于它使用了std::allocator和标准字符特性,basic_string的构造和销毁本身并未被用户覆盖。因此,有一些基础来支持这样一个想法:该字符串的创建不是函数调用的可见副作用,因此可以在"好像"规则下被删除。

然而,由于std::allocator::allocate被指定调用::operator new,而operator new是全局可替换的,因此可以合理地认为这是构造此类字符串的可见副作用。因此,编译器不能在"好像"规则下将其删除。

如果编译器知道您没有替换operator new,理论上它可以优化掉该字符串。
这并不意味着任何特定的编译器都会这样做。

因此,在C++14中,可以省略new表达式(即跳过对运算符::new的调用)或将其组合起来;但是,(a)std::allocator是否以与该省略兼容的方式指定(我希望如此!)(b)标准实际上是否要求std::string使用该分配器来获取内存?(使用SBO,措辞会很棘手...) - Yakk - Adam Nevraumont
1
但我的意思是,“因此,编译器不能根据'仿佛'规则将其删除。”这不正确,因为编译器首先不需要进行任何分配,无论是否替换了operator new() - einpoklum
1
@Jarod42 因为允许小字符串优化,所以很明显分配内存不是 std::string 规范的一部分。 - curiousguy
1
@Jarod42 我也看到过这一点,但是我不会相信cppreference提供的如此详细的信息。我想要查看std分配器的规范。而且这只是第一个参数;第二个参数(即std string并没有说它进行了分配)已经足够说明问题了。 - Yakk - Adam Nevraumont
2
你不应该试图“读懂”cppreference的“字里行间” - 我们并不是为了语言律师级别的精细解析而编写的。但是,如果我们明确表示编译器不允许优化某些内容,那么它很有可能不会这样做。如果我们明确表示实现允许优化某些内容,那么它很有可能会这样做。@Yakk-AdamNevraumont - T.C.
显示剩余10条评论

4

在这里的各种答案和评论中讨论后,我已经对GCC和LLVM提交了以下与此问题有关的错误报告:

  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" };
    }
    
感谢:@JeffGarret,@NicolBolas,@Jarod42,Marc Glisse。
更新,2021年8月:随着clang++,g ++和libstc ++的最新版本,所有这些最小化的测试用例都避免了内存分配,正如人们所期望的那样。clang++ 对于问题中 OP 的程序也有这种行为,但是GCC仍然会分配和释放内存。

2
问题是程序能否
int bar() {
    std::pair<int, std::string> p { 
        123, "Hey... no small-string optimization for me please!" };
    return p.first;
}

可以有效地进行优化

int bar() {
    return 123;
}

简而言之,是的,我认为可以。
clang也使用libc++:godbolt 关于std::string,标准规定如下string.require/3

每个basic_­string类型的对象都使用Allocator类型的对象根据需要分配和释放所包含的charT对象的存储空间。

“根据需要”std::string允许决定何时使用分配器(我相信这是SSO有效的理由)。它的成员函数不强制执行分配。因此,可能会省略分配。

首先,我不知道GodBolt的clang++默认使用libstdc++,所以谢谢你提醒。但是,为什么它愿意使用libc++优化字符串,而不使用libstdc++呢?我的意思是,这些实现不能有太大的差异,对吧? - einpoklum
1
在调查此问题时,我发现在错误跟踪器中有一些影响:(1) gcc通常无法优化分配,(2) 即使是最简单的try/catch方法也会阻碍clang。据我所知,libstdc++在new和delete之间确实有一些try/catch处理。 - Jeff Garrett
我说“影响”,因为我找不到多个来源或证明它们。例如,在这个gcc错误中(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71258),它说“GCC目前无法消除对‘new’的调用。”…但这已经很旧了(2016年)。但它仍然是开放状态。 - Jeff Garrett
这个针对LLVM的bug表明即使在同一个函数中,对于最琐碎的try/catch处理方式,且在clang已经有完整信息的情况下,也无法被优化掉(而人们可以推断,在这样的结构前后折叠逻辑可能会产生干扰):https://bugs.llvm.org/show_bug.cgi?id=35052 - Jeff Garrett
显然,问题的根源并不是异常。请查看我“答案”中的链接。 - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接