与零比较时的int运算符!=和==


75

我发现!=和==并不是测试零或非零的最快方法。

bool nonZero1 = integer != 0;
xor eax, eax
test ecx, ecx
setne al

bool nonZero2 = integer < 0 || integer > 0;
test ecx, ecx
setne al

bool zero1 = integer == 0;
xor eax, eax
test ecx, ecx
sete al

bool zero2 = !(integer < 0 || integer > 0);
test ecx, ecx
sete al

编译器:VC ++ 11优化标志:/ O2 / GL / LTCG

这是x86-32的程序集输出。两种比较的第二个版本在x86-32和x86-64上都快了12%。但是,在x86-64上,指令是相同的(第一个版本看起来与第二个版本完全相同),但是第二个版本仍然更快。

  1. 为什么编译器没有在x86-32上生成更快的版本?
  2. 当汇编输出相同时,为什么第二个版本在x86-64上仍然更快?

编辑:我添加了基准测试代码。零:1544毫秒,1358毫秒非零:1544毫秒,1358毫秒 http://pastebin.com/m7ZSUrcPhttp://anonymouse.org/cgi-bin/anon-www.cgi/http://pastebin.com/m7ZSUrcP

注意:在单个源文件中编译这些函数时,可能不方便定位,因为main.asm变得很大。我在单独的源文件中有zero1,zero2,nonZero1,nonZero2。

EDIT2:可以同时安装VC ++ 11和VC ++ 2010的人运行基准测试代码并发布计时吗?可能确实是VC ++ 11中的错误。


11
您是否将提供用于性能基准测试的完整程序?
詹姆斯·麦克奈利斯

因此,如果只是跳过异或运算,如何保证eax的其余部分为零?
哈罗德

1
xor指令来自哪里?它们看起来与测试无关,因此它应该是周围代码的一部分。
马克·兰瑟姆

2
如果更改订单会怎样?编译器足够聪明,可以知道它第一次测试之前已经xor:ed并且对于下一次测试仍然有效...eax
Andreas Magnusson

2
NFRCR,您是否真的将其作为线性代码进行了基准测试?我以为您只是将它们粘贴在一起以减小帖子的大小。
哈罗德

Answers:


19

编辑:为我的代码看到OP的程序集列表。我怀疑这甚至是VS2011一般错误。这可能只是OP代码的特殊情况错误。我使用clang 3.2,gcc 4.6.2和VS2010照原样运行OP的代码,在所有情况下,最大差异均为〜1%。

刚刚编译了源代码,ne.c并对我的文件和/O2and/GL标志进行了适当的修改。这是来源

int ne1(int n) {
 return n != 0;
 }

 int ne2(int n) {
 return n < 0 || n > 0;
 }

 int ne3(int n) {
 return !(n == 0);
 }

int main() { int p = ne1(rand()), q = ne2(rand()), r = ne3(rand());}

以及相应的程序集:

    ; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.30319.01 

    TITLE   D:\llvm_workspace\tests\ne.c
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB OLDNAMES

EXTRN   @__security_check_cookie@4:PROC
EXTRN   _rand:PROC
PUBLIC  _ne3
; Function compile flags: /Ogtpy
;   COMDAT _ne3
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne3    PROC                        ; COMDAT
; File d:\llvm_workspace\tests\ne.c
; Line 11
    xor eax, eax
    cmp DWORD PTR _n$[esp-4], eax
    setne   al
; Line 12
    ret 0
_ne3    ENDP
_TEXT   ENDS
PUBLIC  _ne2
; Function compile flags: /Ogtpy
;   COMDAT _ne2
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne2    PROC                        ; COMDAT
; Line 7
    xor eax, eax
    cmp eax, DWORD PTR _n$[esp-4]
    sbb eax, eax
    neg eax
; Line 8
    ret 0
_ne2    ENDP
_TEXT   ENDS
PUBLIC  _ne1
; Function compile flags: /Ogtpy
;   COMDAT _ne1
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne1    PROC                        ; COMDAT
; Line 3
    xor eax, eax
    cmp DWORD PTR _n$[esp-4], eax
    setne   al
; Line 4
    ret 0
_ne1    ENDP
_TEXT   ENDS
PUBLIC  _main
; Function compile flags: /Ogtpy
;   COMDAT _main
_TEXT   SEGMENT
_main   PROC                        ; COMDAT
; Line 14
    call    _rand
    call    _rand
    call    _rand
    xor eax, eax
    ret 0
_main   ENDP
_TEXT   ENDS
END

ne2()其中所用的<>||操作员是显然更昂贵。ne1()ne3()分别使用==!=运算符的是terser和等价的。

Visual Studio 2011是beta版。我认为这是一个错误。我使用其他两个编译器(即gcc 4.6.2clang 3.2)进行的测试以及O2优化开关为Windows 7上的所有三个测试(我进行过测试)生成了完全相同的程序集。总结如下:

$ cat ne.c

#include <stdbool.h>
bool ne1(int n) {
    return n != 0;
}

bool ne2(int n) {
    return n < 0 || n > 0;
}

bool ne3(int n) {
    return !(n != 0);
}

int main() {}

用gcc产生:

_ne1:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    testl   %eax, %eax
    setne   %al
    ret
    .cfi_endproc
LFE0:
    .p2align 2,,3
    .globl  _ne2
    .def    _ne2;   .scl    2;  .type   32; .endef
_ne2:
LFB1:
    .cfi_startproc
    movl    4(%esp), %edx
    testl   %edx, %edx
    setne   %al
    ret
    .cfi_endproc
LFE1:
    .p2align 2,,3
    .globl  _ne3
    .def    _ne3;   .scl    2;  .type   32; .endef
_ne3:
LFB2:
    .cfi_startproc
    movl    4(%esp), %ecx
    testl   %ecx, %ecx
    sete    %al
    ret
    .cfi_endproc
LFE2:
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.startup,"x"
    .p2align 2,,3
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB3:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    call    ___main
    xorl    %eax, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE3:

和叮当声:

    .def     _ne1;
    .scl    2;
    .type   32;
    .endef
    .text
    .globl  _ne1
    .align  16, 0x90
_ne1:
    cmpl    $0, 4(%esp)
    setne   %al
    movzbl  %al, %eax
    ret

    .def     _ne2;
    .scl    2;
    .type   32;
    .endef
    .globl  _ne2
    .align  16, 0x90
_ne2:
    cmpl    $0, 4(%esp)
    setne   %al
    movzbl  %al, %eax
    ret

    .def     _ne3;
    .scl    2;
    .type   32;
    .endef
    .globl  _ne3
    .align  16, 0x90
_ne3:
    cmpl    $0, 4(%esp)
    sete    %al
    movzbl  %al, %eax
    ret

    .def     _main;
    .scl    2;
    .type   32;
    .endef
    .globl  _main
    .align  16, 0x90
_main:
    pushl   %ebp
    movl    %esp, %ebp
    calll   ___main
    xorl    %eax, %eax
    popl    %ebp
    ret

我的建议是将此错误提交给Microsoft Connect

注意:我将它们编译为C源代码,因为我认为使用相应的C ++编译器不会在此处进行任何重大更改。


1
您的新测试搞砸了,编译器执行了恒定传播,因为它n = 10始终确定。然后,最重要的是,它完全消除了函数调用,因为未使用结果并且没有副作用。
Ben Voigt 2012年

1
@dirkgently:说到优化器问题,上下文就是一切。
Ben Voigt 2012年

7
这不是一个错误!如果编译后的代码行为正常,怎么可能是一个错误?它表明优化器存在改进的空间,但是每个优化器都有改进的空间。(顺便说一句,这是一个定理。)
TonyK

1
Microsoft Connect上可能会报告Visual C ++错误。
James McNellis

1
另外,值得使用今天刚刚发布的Visual C ++ 2012 RC进行测试。
詹姆斯·麦克奈利斯

122

这是一个很大的问题,但我认为您已成为编译器依赖关系分析的受害者。

编译器只需要清除eax一次高位,而对于第二个版本则保持清除。第二个版本必须付出代价,xor eax, eax除非编译器分析证明它已被第一个版本清除。

通过利用编译器在第一个版本中所做的工作,第二个版本可以“作弊”。

您如何测量时间?是“(循环中的第一个版本,然后是版本2)”,还是“(循环中的第一个版本)之后是(循环中的第二版本)”?

不要在同一个程序中进行这两个测试(而是针对每个版本重新编译),或者如果这样做,请同时测试“版本A优先”和“版本B优先”,看哪一个先来就要付出代价。


作弊的插图:

timer1.start();
double x1 = 2 * sqrt(n + 37 * y + exp(z));
timer1.stop();
timer2.start();
double x2 = 31 * sqrt(n + 37 * y + exp(z));
timer2.stop();

如果timer2持续时间小于timer1持续时间,我们不能得出结论,乘以31比乘以2快。相反,我们意识到编译器执行了常见的子表达式分析,并且代码变为:

timer1.start();
double common = sqrt(n + 37 * y + exp(z));
double x1 = 2 * common;
timer1.stop();
timer2.start();
double x2 = 31 * common;
timer2.stop();

唯一证明的是,乘以31比计算要快common。这一点一点也不令人惊讶-乘法比sqrt和快得多exp


基准测试代码已添加。我分别运行了Benchmark1和Benchmark2,结果相同。唯一的区别是第一个运行的基准测试,然后“预热”并且速度稍慢。
NFRCR 2012年

这在某种程度上是无关的,但是编译器不会优化31与a的乘法(common << 5) - common
马特

3
@Matt:不是浮点乘法;)对于整数乘法,是的,我想大多数编译器都知道这种技巧,但是取决于体系结构,它可能更快或更慢。几乎可以肯定,IMUL乘以2将转换为左移。
Ben Voigt 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.