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

Why does using the ternary operator to return a string generate considerably different code from returning in an equivalent if/else block?

我在玩 Compiler Explorer 时偶然发现了三元运算符的一个有趣行为:

std::string get_string(bool b)
{
    return b ? "Hello" : "Stack-overflow";
}

编译器为此生成的代码(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"

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

std::string better_string(bool b)
{
    if (b)
    {
        return "Hello";
    }
    else
    {
        return "Stack-Overflow";
    }
}

编译器为上述片段生成的代码(带 -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

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

std::string get_string(bool b)
{
    return b ? std::string("Hello") : std::string("Stack-Overflow");
}

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

P.S:在第一个示例中,GCC 会调用 strlen,但 Clang 不会。

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

感谢您的宝贵时间!

抱歉代码墙

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

16 不是这里任何字符串的长度(较长的字符串,带 NUL,只有 15 个字节长);它是 offset 到 return 对象(其地址在 RDI 中传递以支持 RVO),用于指示正​​在使用 small-string 优化(注意分配不足)。长度为 5 或 5+1+8 存储在 R14 中,它存储在 std::string 中并传递给 memcpy(连同 CMOVNE 选择的指针)以加载实际的字符串字节。

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

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

第一个版本 returns 是一个字符串对象,它使用 not-constant 表达式初始化,产生一个字符串文字,因此对于任何其他可变字符串对象,构造函数是 运行 , 因此 memcpy 进行初始化。

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

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

其中一个版本是否无分支并不重要。