TL:DR: current compilers still have bool
missed-optimizations when doing stuff like
(a&&b) ? x : y
. But the reason why is not that they don't assume 0/1, they just suck at this.
bool
的很多用法都是针对局部变量或内联函数的,所以布尔化到0
/1
可以在原始条件下进行优化并进行分支(或cmov或其他任何操作).当bool
个输入/输出确实必须传递/返回不是内联的或真正存储在内存中的东西时,只需担心优化bool
个输入/输出.
Possible optimization guideline:将来自外部源(函数参数/内存)的bool
与位运算符(如a&b
)组合在一起.MSVC和ICC在这方面做得更好.如果对当地的bool
人来说情况更糟的话,我就不知道了.请注意,对于bool
,a&b
只相当于a&&b
,而不是整数类型.2 && 1
是真的,但是2 & 1
是0,这是假的.按位OR没有这个问题.
IDK此准则是否会对通过函数内(或内联的内容)比较设置的本地变量造成伤害.例如,它可能会导致编译器实际生成整数布尔值,而不是在可能的情况下直接使用比较结果.还要注意的是,这似乎对目前的GCC和叮当没有帮助.
是的,x86存储bool
中的C++实现在一个字节中始终是0或1(至少在函数调用边界上,编译器必须尊重要求这一点的ABI/调用约定).
编译器有时确实会利用这一点,例如,对于bool
GT;int
转换,即使是GCC 4.4也只是简单地将零扩展到32位(movzx eax, dil
).Cang和MSVC也是这样做的.C和C++规则要求此转换生成0或1,因此只有在假定bool
函数参数或全局变量的值为0或1是always安全的情况下,此行为才是安全的.
即使是老的编译器通常也会在bool
-gt;int
中利用它,但在其他情况下不会.因此,阿格纳说的原因是错误的:
编译器之所以不做这样的假设,是因为如果变量未初始化或来自未知来源,则它们可能具有其他值.
MSVC CL19确实编写了假定bool
个函数参数为0或1的代码,因此Windows x86-64ABI必须保证这一点.
在x86-64 System V ABI中(Windows除外),0.98版的ChangeLog写着"指定_Bool
(也就是bool
)在调用方被布尔化".我想,甚至在这一变化之前,编译器就已经假设了,但这只记录了编译器已经依赖的东西.x86-64 SysV ABI中的当前语言为:
3.1.2 Data Representation个
布尔值存储在内存对象中时,存储为单字节对象,其值始终为0(false)或1(true).当存储在整数寄存器中时(作为参数传递除外),寄存器的所有8个字节都是有效的;任何非零值都被认为是真的.
第二句话是无稽之谈:ABI无权告诉编译器如何将内容存储在函数内部的寄存器中,而只是存储在不同编译单元(内存/函数参数和返回值)之间的边界.我在on the github page where it's maintained年前报告了这个ABI缺陷.
3.2.3 Parameter passing:
当在寄存器或堆栈中返回或传递类型为_Bool
的值时,位0包含真值,位1至7应为零16.
(脚注16):其他位未指定,因此为the consumer side of those values can rely on it being 0 or 1 when truncated to 8 bit.
i386 System V ABI中的语言是相同的,即IIRC.
任何编译器如果假设一件事(例如转换为int
)为0/1,但在其他情况下无法利用它,则其值为missed optimization.不幸的是,这种遗漏的优化仍然存在,尽管它们比Agner在那段关于编译器always重新布尔化的文章中写的要少得多.
(对于gcc4.6/4.7和clang/msvc,100上的Source+ASM.另请参阅Matt Godbolt的CppCon2017 Talk What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid)
bool logical_or(bool a, bool b) { return a||b; }
# gcc4.6.4 -O3 for the x86-64 System V ABI
test dil, dil # test a against itself (for non-zero)
mov eax, 1
cmove eax, esi # return a ? 1 : b;
ret
即使是gcc4.6没有将b
重新布尔化,但它确实错过了gcc4的优化.7 makes:(以及其他答案中所示的clang和更高版本的编译器):
# gcc4.7 -O3 to present: looks ideal to me.
mov eax, esi
or eax, edi
ret
(Clang的or dil, sil
/mov eax, edi
是愚蠢的:在写入dil
之后读取edi
时,它肯定会在Nehalem或更早版本的Intel上造成部分寄存器暂停,并且由于需要REX前缀来使用edi的低8部分,它的代码量更大.如果你想避免reading个32位寄存器,以防你的调用者留下一些arg通过,更好的 Select 可能是or dil,sil
/movzx eax, dil
带有"脏"部分寄存器的寄存器.)
MSVC emits this code that checks 101 then 102 separately, completely failing to take advantage of anything,甚至用xor al,al
而不是xor eax,eax
.因此,它对大多数CPU(including Haswell/Skylake, which don't rename low-8 partial regs separately from the whole register, only AH/BH/...)上的旧值eax
有错误的依赖性.这太蠢了.使用xor al,al
的唯一原因是当您明确希望保留较高的字节时.
logical_or PROC ; x86-64 MSVC CL19
test cl, cl ; Windows ABI passes args in ecx, edx
jne SHORT $LN3@logical_or
test dl, dl
jne SHORT $LN3@logical_or
xor al, al ; missed peephole: xor eax,eax is strictly better
ret 0
$LN3@logical_or:
mov al, 1
ret 0
logical_or ENDP
ICC18也没有利用输入的已知0/1特性,它只使用or
指令来根据两个输入的逐位或来设置标志,而使用setcc
指令来产生0/1.
logical_or(bool, bool): # ICC18
xor eax, eax #4.42
movzx edi, dil #4.33
movzx esi, sil #4.33
or edi, esi #4.42
setne al #4.42
ret #4.42
即使对于bool bitwise_or(bool a, bool b) { return a|b; }
,ICC也会发出相同的代码.它提升到int
(具有movzx
),并使用or
根据逐位OR设置标志.与or dil,sil
/setne al
相比,这是愚蠢的.
对于bitwise_or
,MSVC只使用or
指令(在每个输入movzx
之后),但无论如何不会重新布尔化.
当前GCC/clang中错过的优化:
只有icc/msvc才会用上面的简单函数做哑巴代码,但是这个函数还是给GCC和clang带来了麻烦:
int select(bool a, bool b, int x, int y) {
return (a&&b) ? x : y;
}
100(相同的源代码,与上次 Select 的编译器不同).
看起来很简单;您会希望一个聪明的编译器能用test
/cmov
来无分支地完成这项工作.x86的test
指令根据按位AND设置标志.这是一条AND指令,实际上并不写入目的地.(就像cmp
是不写目的地的sub
一样).
# hand-written implementation that no compilers come close to making
select:
mov eax, edx # retval = x
test edi, esi # ZF = ((a & b) == 0)
cmovz eax, ecx # conditional move: return y if ZF is set
ret
但即使是每天在Godbolt编译器浏览器上构建的gcc和clang,也会生成much个更复杂的代码,分别判断每个布尔值.如果返回ab
,他们知道如何优化bool ab = a&&b;
,但即使以这种方式编写(使用一个单独的布尔变量来保存结果),也无法让他们手工编写出不糟糕的代码.
请注意,test same,same
is exactly equivalent to cmp reg, 0
和更小,所以编译器使用它.
Clang's版本严格来说比我的手写版本差.(请注意,它要求调用方将bool
个参数零扩展到32位,like it does for narrow integer types as an unofficial part of the ABI which it and gcc implement but only clang depends on).
select: # clang 6.0 trunk 317877 nightly build on Godbolt
test esi, esi
cmove edx, ecx # x = b ? y : x
test edi, edi
cmove edx, ecx # x = a ? y : x
mov eax, edx # return x
ret
每晚为此编写分支代码,类似于较早的GCC版本所做的工作.
select(bool, bool, int, int): # gcc 8.0.0-pre 20171110
test dil, dil
mov eax, edx ; compiling with -mtune=intel or -mtune=haswell would keep test/jcc together for macro-fusion.
je .L8
test sil, sil
je .L8
rep ret
.L8:
mov eax, ecx
ret
MSVC x86-64 CL19生成非常相似的分支代码.它的目标是Windows调用约定,其中整数参数在RCX、RDX、R8、R9中.
select PROC
test cl, cl ; a
je SHORT $LN3@select
mov eax, r8d ; retval = x
test dl, dl ; b
jne SHORT $LN4@select
$LN3@select:
mov eax, r9d ; retval = y
$LN4@select:
ret 0 ; 0 means rsp += 0 after popping the return address, not C return 0.
; MSVC doesn't emit the `ret imm16` opcode here, so IDK why they put an explicit 0 as an operand.
select ENDP
ICC18也生成分支代码,但是在分支之后都有mov
条指令.
select(bool, bool, int, int):
test dil, dil #8.13
je ..B4.4 # Prob 50% #8.13
test sil, sil #8.16
jne ..B4.5 # Prob 50% #8.16
..B4.4: # Preds ..B4.2 ..B4.1
mov edx, ecx #8.13
..B4.5: # Preds ..B4.2 ..B4.4
mov eax, edx #8.13
ret #8.13
Trying to help the compiler by using个
int select2(bool a, bool b, int x, int y) {
bool ab = a&&b;
return (ab) ? x : y;
}
leads MSVC into making hilariously bad code:
;; MSVC CL19 -Ox = full optimization
select2 PROC
test cl, cl
je SHORT $LN3@select2
test dl, dl
je SHORT $LN3@select2
mov al, 1 ; ab = 1
test al, al ;; and then test/cmov on an immediate constant!!!
cmovne r9d, r8d
mov eax, r9d
ret 0
$LN3@select2:
xor al, al ;; ab = 0
test al, al ;; and then test/cmov on another path with known-constant condition.
cmovne r9d, r8d
mov eax, r9d
ret 0
select2 ENDP
这仅适用于MSVC(ICC18在刚刚设置为常数的寄存器上具有相同的test/cmov遗漏优化).
GCC和Cang一如既往地编写的代码不会像msvc那样糟糕;他们编写的asm和msvc一样,这仍然不是很好,但至少试图帮助他们不会像msvc那样让情况变得更糟.
Combine bool
with bitwise operators helps MSVC and ICC
在我非常有限的测试中,对于MSVC和ICC,|
和&
似乎比||
和&&
工作得更好.使用编译器+编译选项查看您自己的代码的编译器输出,看看会发生什么.
int select_bitand(bool a, bool b, int x, int y) {
return (a&b) ? x : y;
}
Gcc still branches separately在两个输入的单独test
个上,代码与select
的其他版本相同.clang still does two separate 102,与其他源版本的ASM相同.
MSVC通过并正确优化,击败了所有其他编译器(至少在独立定义中):
select_bitand PROC ;; MSVC
test cl, dl ;; ZF = !(a & b)
cmovne r9d, r8d
mov eax, r9d ;; could have done the mov to eax in parallel with the test, off the critical path, but close enough.
ret 0
ICC18浪费了两条movzx
指令,将bool
s扩展为int
,然后生成与MSVC相同的代码
select_bitand: ## ICC18
movzx edi, dil #16.49
movzx esi, sil #16.49
test edi, esi #17.15
cmovne ecx, edx #17.15
mov eax, ecx #17.15
ret #17.15