编译器中的布尔值为8位。对他们的操作是否效率低下?

时间:2017-11-11 23:35:53

标签: c++ c optimization x86 boolean

我正在阅读Agner Fog" Optimizing software in C++" (特定于Intel,AMD和VIA的x86处理器),并在第34页说明

  

布尔变量存储为8位整数,值0表示false,1表示true。   布尔变量在所有具有布尔值的运算符的意义上都是超定的   变量作为输入检查输入是否具有除0或1之外的任何其他值,但运算符是否为   将布尔值作为输出可以产生除0或1之外的其他值。这使得操作成为可能   使用布尔变量作为输入效率低于必要的效率。

今天是否仍然如此以及编译器是什么?你能举个例子吗?作者陈述

  

如果布尔运算可以更有效率   众所周知,操作数没有其他值而不是0和1.原因   为什么编译器没有做出这样的假设,即变量可能有其他变量   如果它们未初始化或来自未知来源,则为值。

这是否意味着如果我以函数指针bool(*)()为例并调用它,那么对它的操作会产生效率低下的代码?或者是通过取消引用指针或从引用读取然后对其进行操作来访问布尔值的情况?

3 个答案:

答案 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之后),但无论如何都不会重新布尔化。

错过了当前gcc / clang中的优化:

只有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版本正在做的更少。