【问题标题】:Is this "elision failure" language-mandated?这种“省略失败”是语言规定的吗?
【发布时间】:2023-03-12 18:00:02
【问题描述】:

考虑以下代码:

#include <utility>
#include <string>

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

(感谢@Jarod42 :-) ...)

我希望该功能可以简单地实现:

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() 之间,编译器知道 AFAICT 不会导致任何用处。
  2. 其次,new()delete() 调用自己。毕竟,标准 AFAIK 允许小字符串优化,所以即使 clang/gcc 没有选择使用它 - 它可以有;这意味着实际上不需要在那里调用new()delete()

我特别感兴趣的是,这其中的哪一部分直接归因于语言标准,哪一部分是编译器的非最优性。

【问题讨论】:

  • new/delete 不是可观察到的行为吗? (和new 可以throw
  • @Jarod42: 1. 我不确定在 C++20 中是否可以使用 constexpr 向量。 2. 即使是 - 如果理论上允许您使用最多 1,000,000 个字符的小字符串优化,这意味着您可以为 foo()bar() 提供不同的可观察行为。
  • 根据en.cppreference.com/w/cpp/language/new#Allocation,分配只能从C++14开始省略。
  • inconsistent-behavior-of-compiler-optimization-of-unused-string 显示delete new int; 的编译器问题
  • 我会说 SSO 应该是一致的,否则它主要是 ODR 违规。

标签: c++ language-lawyer compiler-optimization stdstring elision


【解决方案1】:

您的代码中没有任何内容代表"elision" as that term is commonly used in a C++ context。不允许编译器以“省略”为由从该代码中删除任何内容。

编译器必须删除该字符串的创建的唯一理由是基于“好像”规则。也就是说,字符串创建/销毁的行为是否对用户可见,因此无法删除?

由于它使用std::allocator 和标准字符特征,basic_string 构造和销毁本身不会被用户覆盖。因此,字符串的创建不是函数调用的可见副作用,因此可以根据“好像”规则删除。

但是,因为std::allocator::allocate 被指定为调用::operator new,并且operator new 是全局可替换的,所以有理由认为这是构造这样一个字符串的可见副作用。因此,编译器无法根据“好像”规则将其删除。

如果编译器知道你没有替换operator new,那么理论上它可以优化掉字符串。

这并不意味着任何特定的编译器这样做。

【讨论】:

  • 所以new 表达式可以被省略(即跳过,不调用运算符::new)或从C++14 开始组合;但是(a)std::allocator 是否以与该省略兼容的方式指定,(我希望如此!)(b)标准是否实际上要求std::string 使用该分配器来获取内存? (对于 SBO,这个措辞会很棘手......)
  • 但我的意思是“因此,编译器无法根据“好像”规则将其删除。不正确,因为不管operator new()是否被替换,编译器一开始就不需要做任何分配。
  • @Jarod42 因为允许小 str 优化,所以很明显,分配不是std::string 规范的一部分。
  • @Jarod42 我也读过,但我不相信 cppreference 的详细程度。我想检查标准分配器的规范。那只是论点1;参数 2(std 字符串没有说​​它分配)足够强大,可以携带它。
  • 您不应该尝试“阅读” cppreference 的“字里行间”——我们不是为语言律师级别的精细解析而编写的。但是如果我们明确地说不允许编译器优化某些东西,那么很有可能它不是。如果我们明确地说the implementation is allowed to optimize something out,那么很有可能就是这样。 @Yakk-AdamNevraumont
【解决方案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;
}

tldr,是的,我想。

clang 使用 libc++:godbolt

关于std::string,标准说string.require/3

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

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

【讨论】:

  • 首先,我不知道 GodBolt 的 clang++ 默认为 libstdc++,所以谢谢。但是,为什么它愿意用 libc++ 优化字符串,而不用 libstdc++?我的意思是,实现不能那么不同,可以吗?
  • 在研究这一点时,我发现错误跟踪器中的含义是(1)gcc 无法优化分配,以及(2)clang 被最简单的 try/catch 挫败。据我所知,libstdc++ 在 new 和 delete 之间确实有一些 try/catch。
  • 我说的是暗示,因为我无法找到多个来源或证实它们。例如,在这个 gcc 错误 (gcc.gnu.org/bugzilla/show_bug.cgi?id=71258) 中,它说“GCC 目前无法消除对 'new' 的调用”。 ...但它已经过时了(2016 年)。但它也仍然开放。
  • 这个 LLVM 的 bug 显示了即使是最琐碎的 try/catch,在同一个函数中处理,完整的知识可供 clang 使用,也无法优化掉(并且可能会导致这会干扰折叠这种构造之前和之后的逻辑):bugs.llvm.org/show_bug.cgi?id=35052
  • 显然,问题的原因不是异常。请参阅我的“答案”中的链接。
【解决方案3】:

根据此处各种答案和 cmets 的讨论,我现在针对 GCC 和 LLVM 提交了以下关于此问题的错误:

  1. GCC bug 94293: [错过优化] new+delete of unused local string not removed

    最小测试用例 (GodBolt):

    void foo() {
        int *p = new int[1];
        *p = 42;
        delete[] p;
    }
    
  2. GCC bug 94294: [错过优化] 填充本地字符串的无用语句未删除

    最小测试用例(GodBolt):

    void foo() {
        std::string s { "This is not a small string" };
    }
    
  3. LLVM bug 45287: [错过优化] 未能删除未使用的 libstdc++ std::string

    最小测试用例(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 仍然分配和解除分配。

【讨论】:

    猜你喜欢
    • 2014-02-25
    • 2012-02-19
    • 2014-09-21
    • 1970-01-01
    • 2013-10-08
    • 2013-03-01
    • 2020-05-05
    • 2018-02-22
    • 2021-01-18
    相关资源
    最近更新 更多