Int 运算符! = 和 = = 与零相比

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

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上仍然更快?

编辑: 我添加了基准测试代码。0:1544ms,1358ms 非零: 1544ms,1358ms Http://pastebin.com/m7zsurcp 或者 Http://anonymouse.org/cgi-bin/anon-www.cgi/http://pastebin.com/m7zsurcp

注意: 在单个源文件中编译这些函数可能不太方便,因为 main.asm 非常大。我在一个单独的源文件中找到了 zero 1,zero2,nonZero1,nonZero2。

编辑2: 同时安装了 VC + + 11和 VC + + 2010的人可以运行基准测试代码并发布计时吗?这可能确实是 VC + + 11中的一个 bug。

3191 次浏览

这是一个很好的问题,但我认为您已经成为编译器依赖性分析的牺牲品。

编译器只需清除 eax的高位一次,在第二个版本中它们仍然是清除的。第二个版本将不得不为 xor eax, eax付出代价,除非编译器分析证明它已被第一个版本清除。

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

你怎么计时的?是“(版本一,后面跟着版本二)在循环中”,还是“(版本一在循环中)后面跟着(版本二在循环中)”?

不要在同一个程序中同时进行两个测试(而是为每个版本重新编译) ,或者如果你这样做了,同时测试“版本 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要快。这一点也不奇怪——增殖远远快于 sqrtexp

编辑: 看到 OP 为我的代码编写的程序集列表。我甚至怀疑这是一个 VS2011通用漏洞现在。这可能只是 OP 代码的一个特例 bug。我使用 clang 3.2、 gcc 4.6.2和 VS2010运行 OP 的代码,在所有情况下,最大差异都在1% 左右。

只是对我的 ne.c文件以及 /O2/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()是更简洁和等价的。

VisualStudio2011是 测试版。我认为这是一个错误。我用另外两个编译器,即 Gcc4.6.2叮当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() {}

与海湾合作委员会的收益率:

_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

我的建议是在 微软连接中将其作为错误归档。

注意: 我将它们编译为 C 源代码,因为我不认为使用相应的 C + + 编译器会在这里做出任何重大改变。