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



我在玩编译器资源管理器时,在使用以下内容时偶然发现了三元运算符的一个有趣行为:

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的调用,并且不关心同时知道两个字符串的长度。有两个不同的标签,它跳到

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没有。

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

感谢您抽出时间!

对不起代码墙

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

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

另一个版本有一个明显的分支(尽管std::string结构的一部分已经被提升到它之上(,并且实际上明确地有5和14,但由于字符串字节被包括为各种大小的立即值(表示为整数(,这一事实使它变得模糊。

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

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

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

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

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

最新更新