为什么使用三元运算符返回字符串会生成与在等效的 if/else 块中返回的代码截然不同的代码?

Mar*_*s T 71 c++ optimization assembly clang compiler-optimization

我正在使用编译器资源管理器,在使用类似这样的东西时,我偶然发现了三元运算符的一个有趣行为:

std::string get_string(bool b)
{
    return b ? "Hello" : "Stack-overflow";
}
Run Code Online (Sandbox Code Playgroud)

编译器为此生成的代码(clang trunk,带 -O3)是这样的:

get_string[abi:cxx11](bool):                 # @get_string[abi:cxx11](bool)
        push    r15
        push    r14
        push    rbx
        mov     rbx, rdi
        mov     ecx, offset .L.str
        mov     eax, offset .L.str.1
        test    esi, esi
        cmovne  rax, rcx
        add     rdi, 16 #< Why is the compiler storing the length of the string
        mov     qword ptr [rbx], rdi
        xor     sil, 1
        movzx   ecx, sil
        lea     r15, [rcx + 8*rcx]
        lea     r14, [rcx + 8*rcx]
        add     r14, 5 #< I also think this is the length of "Hello" (but not sure)
        mov     rsi, rax
        mov     rdx, r14
        call    memcpy #< Why is there a call to memcpy
        mov     qword ptr [rbx + 8], r14
        mov     byte ptr [rbx + r15 + 21], 0
        mov     rax, rbx
        pop     rbx
        pop     r14
        pop     r15
        ret
.L.str:
        .asciz  "Hello"

.L.str.1:
        .asciz  "Stack-Overflow"
Run Code Online (Sandbox Code Playgroud)

但是,编译器为以下代码段生成的代码要小得多,并且没有调用memcpy,并且不关心同时知道两个字符串的长度。它跳转到 2 个不同的标签

std::string better_string(bool b)
{
    if (b)
    {
        return "Hello";
    }
    else
    {
        return "Stack-Overflow";
    }
}
Run Code Online (Sandbox Code Playgroud)

编译器为上述片段(带有 -O3 的 clang trunk)生成的代码是这样的:

better_string[abi:cxx11](bool):              # @better_string[abi:cxx11](bool)
        mov     rax, rdi
        lea     rcx, [rdi + 16]
        mov     qword ptr [rdi], rcx
        test    sil, sil
        je      .LBB0_2
        mov     dword ptr [rcx], 1819043144
        mov     word ptr [rcx + 4], 111
        mov     ecx, 5
        mov     qword ptr [rax + 8], rcx
        ret
.LBB0_2:
        movabs  rdx, 8606216600190023247
        mov     qword ptr [rcx + 6], rdx
        movabs  rdx, 8525082558887720019
        mov     qword ptr [rcx], rdx
        mov     byte ptr [rax + 30], 0
        mov     ecx, 14
        mov     qword ptr [rax + 8], rcx
        ret
Run Code Online (Sandbox Code Playgroud)

相同的结果是当我使用三元运算符时:

std::string get_string(bool b)
{
    return b ? std::string("Hello") : std::string("Stack-Overflow");
}
Run Code Online (Sandbox Code Playgroud)

我想知道为什么第一个示例中的三元运算符会生成该编译器代码。我相信罪魁祸首在const char[].

PS:strlen在第一个示例中,GCC 会调用 to ,但 Clang 不会。

链接到编译器资源管理器示例:https : //godbolt.org/z/Exqs6G

感谢您的时间!

抱歉代码墙

Dav*_*ing 60

这里的主要区别是第一个版本是无分支的

16 不是这里任何字符串的长度(较长的字符串,带 NUL,只有 15 个字节长);它是返回对象的偏移量(其地址在 RDI 中传递以支持 RVO),用于指示正​​在使用小字符串优化(注意缺少分配)。长度为 5 或 5+1+8 存储在 R14 中,它存储在std::string以及传递给memcpy(与 CMOVNE 选择的指针一起)以加载实际字符串字节。

另一个版本有一个明显的分支(虽然部分std::string结构已经被提升到它上面)并且实际上确实明确地有 5 和 14,但是由于字符串字节已被包含为立即数(表示为整数)的事实而混淆了各种尺寸。

至于为什么这三个等效函数会产生两种不同版本的生成代码,我只能提供优化器是迭代和启发式算法;他们不能可靠地独立于起点找到相同的“最佳”装配。

  • 值得注意的是,在这种情况下,人们应该注意到内存写入的优化要困难得多——即使“memcpy”是内部固有的,优化器仍然需要推理写入迟早发生的潜在副作用。在第一个片段中,对三元表达式进行求值,然后发生写入,在第二个片段中,写入作为三元表达式求值的一部分发生。 (4认同)
  • @cmaster-reinstatemonica:无分支只是对一种情况下生成的程序集的描述(这有助于理解其他差异)。在此处的所有情况下,构造函数都是完全内联的(“在编译时评估”);return 语句的操作数的类型绝不是对生成代码的约束(因为没有任何字符串文字的地址转义)。 (4认同)
  • 我同意,它_不应该_,但正如你所提到的,因为优化器是迭代和启发式的,......它确实如此并不令人惊讶:) (2认同)
  • 没有分支在这里是一个转移注意力的事情。于尔根的答案是正确的。区别在于执行选择的类型(“std::string”与“char*”),以及是否需要使用选择结果调用构造函数。 (2认同)
  • 如果编译器能够将常量值传播分析执行到最后,它应该在所有三种情况下生成完全相同的输出。但事实并非如此。所以,显然,它还没有完成分析。显然,它在第一种情况下必须使用两个可能的参数之一构造单个对象,而在其他情况下需要选择两个不同对象的构造,这一事实被抛弃了。第一个和最后一个代码示例的行为之间的比较很有启发性。 (2认同)

Jue*_*gen 13

第一个版本返回一个字符串对象,该对象用一个非常量表达式初始化,产生一个字符串文字,因此构造函数像任何其他变量字符串对象一样运行,因此 memcpy 进行初始化。

其他变体返回用字符串文字初始化的一个字符串对象或用另一个字符串文字初始化的另一个字符串对象,这两者都可以优化为从不需要 memcpy 的常量表达式构造的字符串对象。

所以真正的答案是:第一个版本在初始化对象之前对 char[] 表达式操作 ?: 运算符,其他版本对已经初始化的字符串对象进行操作。

版本之一是否是无分支的并不重要。

  • 无分支汇编中也不需要真正需要“memcpy”;与在立即操作数上使用更多“cmov”指令或 SSE2 比较相比,这是一个错过的优化。不过,您的回答确实解释了为什么源代码引导编译器朝它的方向发展;编译器远非完美。 (4认同)
  • 请注意,在 OP 的 Godbolt 链接中,所有 3 个版本均未注释,https://godbolt.org/z/597Kzd,`return b ? std::string("Hello") : std::string("Stack-Overflow");` 使用 GCC 和 clang 编译为分支(与 `if` 版本相同),尽管常量传播有机会使const `string` 对象。 (3认同)