我正在阅读Agner Fog" Optimizing software in C++" (特定于Intel,AMD和VIA的x86处理器),并在第34页说明
布尔变量存储为8位整数,值0表示false,1表示true。 布尔变量在所有具有布尔值的运算符的意义上都是超定的 变量作为输入检查输入是否具有除0或1之外的任何其他值,但运算符是否为 将布尔值作为输出可以产生除0或1之外的其他值。这使得操作成为可能 使用布尔变量作为输入效率低于必要的效率。
今天是否仍然如此以及编译器是什么?你能举个例子吗?作者陈述
如果布尔运算可以更有效率 众所周知,操作数没有其他值而不是0和1.原因 为什么编译器没有做出这样的假设,即变量可能有其他变量 如果它们未初始化或来自未知来源,则为值。
这是否意味着如果我以函数指针bool(*)()
为例并调用它,那么对它的操作会产生效率低下的代码?或者是通过取消引用指针或从引用读取然后对其进行操作来访问布尔值的情况?
答案 0 :(得分:68)
TL:DR :当执行类似事情时,当前编译器仍然有bool
次错误优化
(a&&b) ? x : y
。但是不的原因是他们不假设0/1,他们只是嘲笑它。
bool
的许多用途适用于本地或内联函数,因此对0
/ 1
进行布尔运算可以优化原始条件下的分支(或cmov或其他)。只有在必须通过不内联或真正存储在内存中的内容传递/返回时,才会担心优化bool
输入/输出。
可能的优化准则:将来自外部源(函数args /内存)的bool
与按位运算符组合在一起,例如a&b
。 MSVC和ICC做得更好。 IDK,如果本地bool
更糟糕的话。请注意,a&b
仅相当于a&&b
的{{1}},而不是整数类型。 bool
为真,但2 && 1
为0,这是假的。按位或没有这个问题。
如果此指南对于通过函数内部比较设置的本地人(或内联的内容)会受到伤害,则会发生IDK。例如。它可能会导致编译器实际生成整数布尔值,而不是直接使用比较结果。另请注意,它似乎无助于当前的gcc和clang。
是的,x86上的C ++实现将2 & 1
存储在一个始终为0或1的字节中(至少跨越函数调用边界,其中编译器必须遵守需要此的ABI /调用约定。 )
编译器有时会利用这一点,例如:对于bool
- > bool
转换,甚至gcc 4.4只是零扩展到32位(int
)。 Clang和MSVC也这样做。 C和C ++规则要求此转换生成0或1,因此只有当总是安全地认为movzx eax, dil
函数arg或全局变量具有0或1值。
即使是旧的编译器通常也会在bool
- > bool
中利用它,但在其他情况下却没有。因此,当Agner说:
编译器没有做出这样的假设的原因是,如果变量未初始化或来自未知来源,变量可能具有其他值。
MSVC CL19确实使代码假定int
函数args为0或1,因此Windows x86-64 ABI必须保证这一点。
在x86-64 System V ABI(由Windows以外的所有内容使用)中,修订版0.98的更改日志表示"指定bool
(aka _Bool
)在调用者处被boolean化。 "我认为即使在这种变化之前,编译器也会假设它,但这仅仅记录了编译器已经依赖的内容。 x86-64 SysV ABI中的当前语言是:
3.1.2数据表示
布尔值存储在内存对象中时,存储为单字节对象,其值始终为0(假)或1(真)。当存储在整数寄存器中时(除了作为参数传递),寄存器的所有8个字节都是重要的;任何非零值都被视为真。
第二句是废话:ABI没有告诉编译器如何在函数内的寄存器中存储东西,只在不同编译单元之间的边界(内存/函数args和返回值)。我刚刚报告了这个ABI缺陷on the github page where it's maintained。
3.2.3参数传递:
当在寄存器或堆栈中返回或传递类型
bool
的值时,位0包含真值,位1到7应为零 16 。(脚注16):未指定其他位,因此这些值的消费者端在截断为8位时可依赖于0或1。
i386 System V ABI中的语言与IIRC相同。
任何编译器假设一件事情为0/1(例如转换为_Bool
)但在其他情况下未能利用它有错过优化。不幸的是,这种遗漏优化仍然存在,尽管它们比Agner写的关于编译器始终重新布尔化的段落更为罕见。
(源代码+ asm在 Godbolt compiler explorer 上为gcc4.6 / 4.7和clang / MSVC。另见Matt Godbolt的CppCon2017演讲What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid)< / p>
int
所以即使gcc4.6没有重新布尔化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.7所做的优化:(以及其他答案中显示的clang和后来的编译器):
b
(Clang&#39; s # gcc4.7 -O3 to present: looks ideal to me.
mov eax, esi
or eax, edi
ret
/ or dil, sil
很愚蠢:在写完{1}后,它确保在Nehalem或早期英特尔上造成部分注册失效mov eax, edi
,代码大小更差,因为需要使用REX前缀来使用edi的低8部分。如果你想避免edi / dil
>读取任何32位寄存器,以防你的调用者使用&#34;脏&#34;部分寄存器留下一些arg-passing寄存器。)
MSVC会发出此代码,分别检查or dil,sil
然后movzx eax, dil
,完全无法利用任何内容,甚至使用a
代替{{1} }}。因此它在大多数CPU(including Haswell/Skylake, which don't rename low-8 partial regs separately from the whole register, only AH/BH/...)上对b
的旧值具有错误的依赖性。这只是愚蠢的。使用xor al,al
的唯一原因是当您明确要保留高位字节时。
xor eax,eax
ICC18也没有利用输入的已知0/1特性,它只使用eax
指令根据两个输入的按位OR设置标志,{{1产生0/1。
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
,ICC也会发出相同的代码。它会升级到or
(使用setcc
),并使用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
根据按位OR设置标志。与bool bitwise_or(bool a, bool b) { return a|b; }
/ int
相比,这是愚蠢的。
对于movzx
,MSVC只使用or
指令(在每个输入上or dil,sil
之后),但无论如何都不会重新布尔化。
只有ICC / MSVC使用上面的简单函数制作哑代码,但是这个函数仍然会给gcc和clang带来麻烦:
setne al
Source+asm on the Godbolt compiler explorer (相同来源,选择的编译器与上次不同)。
看起来很简单;您希望智能编译器能够使用bitwise_or
/ or
进行无分支处理。 x86&#39; movzx
指令根据按位AND设置标志。它是一个没有实际写入目的地的AND指令。 (就像int select(bool a, bool b, int x, int y) {
return (a&&b) ? x : y;
}
是test
并不会写出目的地一样。)
cmov
但即使是Godbolt编译器资源管理器上的gcc和clang的每日构建,也会使很多更复杂的代码,分别检查每个布尔值。如果你返回test
,他们知道如何优化cmp
,但即使以那种方式编写它(用一个单独的布尔变量来保存结果)也不会设法将它们手工制作成代码这并不难过。
请注意test same,same
is exactly equivalent to cmp reg, 0
,并且更小,因此它是编译器使用的。
Clang的版本严格地比我的手写版本差。 (请注意,它要求调用者将sub
args零扩展为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)。
# 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
gcc 8.0.0 20171110 每晚为此制作分支代码,类似于旧版gcc版本。
bool ab = a&&b;
MSVC x86-64 CL19 制作非常相似的分支代码。它的目标是Windows调用约定,其中整数args位于rcx,rdx,r8,r9中。
ab
ICC18 也会生成分支代码,但在分支后有bool
条指令。
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
尝试使用
帮助编译器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制作搞笑的错误代码:
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
这仅适用于MSVC(并且ICC18在已设置为常量的寄存器上具有相同的test / cmov错过优化)。
像往常一样,gcc和clang不会使代码像MSVC一样糟糕;他们为mov
做了同样的事情,这仍然不好,但至少试图帮助他们并不像MSVC那样让事情变得更糟。
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
与按位运算符组合可帮助MSVC和ICC 在我非常有限的测试中,对于MSVC和ICC,int select2(bool a, bool b, int x, int y) {
bool ab = a&&b;
return (ab) ? x : y;
}
和;; 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
似乎比select()
和bool
效果更好。使用编译器+编译选项查看您自己代码的编译器输出,看看会发生什么。
|
Gcc仍然在两个输入的单独&
上单独分支,与||
的其他版本相同的代码。 clang仍会执行两个单独的&&
,与其他源版本相同。
MSVC正确地进行并优化,击败所有其他编译器(至少在独立定义中):
int select_bitand(bool a, bool b, int x, int y) {
return (a&b) ? x : y;
}
ICC18浪费了两条test
指令,将select
零扩展到test/cmov
,但后来生成与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
答案 1 :(得分:7)
我认为事实并非如此。
首先,这种推理是完全不可接受的:
编译器没有做出这样的假设的原因是 如果变量未初始化,则变量可能具有其他值 来自不明来源。
让我们检查一些代码(使用clang 6编译,但GCC 7和MSVC 2017会生成相似的代码)。
布尔或:
bool fn(bool a, bool b) {
return a||b;
}
0000000000000000 <fn(bool, bool)>:
0: 40 08 f7 or dil,sil
3: 40 88 f8 mov al,dil
6: c3 ret
可以看出,这里没有0/1检查,简单or
。
将bool转换为int:
int fn(bool a) {
return a;
}
0000000000000000 <fn(bool)>:
0: 40 0f b6 c7 movzx eax,dil
4: c3 ret
再次,没有检查,简单的移动。
将char转换为bool:
bool fn(char a) {
return a;
}
0000000000000000 <fn(char)>:
0: 40 84 ff test dil,dil
3: 0f 95 c0 setne al
6: c3 ret
这里,检查char是否为0,并将bool值设置为0或1。
所以我认为可以安全地说编译器以某种方式使用bool,因此它总是包含0/1。它永远不会检查它的有效性。
关于效率:我认为bool是最佳的。我能想象的唯一一种情况,即这种方法不是最优的是char-&gt; bool转换。如果bool值不被限制为0/1,那么该操作可以是简单的mov。对于所有其他操作,当前的方法同样好或更好。
编辑:Peter Cordes提到了ABI。这里是AMD64 System V ABI的相关文本(i386的文字类似):布尔值,存储在内存对象中时,存储为单字节 对象的值始终为0(假)或1(真)。什么时候 存储在整数寄存器中(除了作为参数传递),全部为8 寄存器的字节很重要;考虑任何非零值 真
因此,对于遵循SysV ABI的平台,我们可以确定bool
的值为0/1。
我搜索了MSVC的ABI文档,但不幸的是我没有找到关于bool
的任何内容。
答案 2 :(得分:0)
我使用clang ++ -O3 -S
编译了以下内容bool andbool(bool a, bool b)
{
return a && b;
}
bool andint(int a, int b)
{
return a && b;
}
.s
文件包含:
andbool(bool, bool): # @andbool(bool, bool)
andb %sil, %dil
movl %edi, %eax
retq
andint(int, int): # @andint(int, int)
testl %edi, %edi
setne %cl
testl %esi, %esi
setne %al
andb %cl, %al
retq
显然,这是bool版本正在做的更少。