int opérateurs! = et == quand on compare à zéro

J’ai trouvé que! = Et == ne sont pas les moyens les plus rapides pour tester zéro ou non nul.

bool nonZero1 = integer != 0; xor eax, eax test ecx, ecx setne al bool nonZero2 = integer  0; test ecx, ecx setne al bool zero1 = integer == 0; xor eax, eax test ecx, ecx sete al bool zero2 = !(integer  0); test ecx, ecx sete al 

Compilateur: Indicateurs d’optimisation VC ++ 11: / O2 / GL / LTCG

Ceci est la sortie de l’assemblage pour x86-32. Les deuxièmes versions des deux comparaisons étaient ~ 12% plus rapides à la fois sur x86-32 et x86-64. Cependant, sur x86-64, les instructions étaient identiques (les premières versions ressemblaient exactement aux deuxièmes versions), mais les deuxièmes versions étaient encore plus rapides.

  1. Pourquoi le compilateur ne génère-t-il pas la version plus rapide sur x86-32?
  2. Pourquoi les deuxièmes versions sont-elles encore plus rapides sur x86-64 lorsque la sortie de l’assemblage est identique?

EDIT: J’ai ajouté le code de référence. ZERO: 1544ms, 1358ms NON_ZERO: 1544ms, 1358ms http://pastebin.com/m7ZSUrcP ou http://anonymouse.org/cgi-bin/anon-www.cgi/http://pastebin.com/m7ZSUrcP

Remarque: il est probablement peu pratique de localiser ces fonctions lors de la compilation dans un seul fichier source, car le fichier main.asm est assez volumineux. J’ai eu zero1, zero2, nonZero1, nonZero2 dans un fichier source séparé.

EDIT2: Quelqu’un avec à la fois VC ++ 11 et VC ++ 2010 peut-il exécuter le code d’évaluation et publier les délais? Il pourrait en effet s’agir d’un bogue dans VC ++ 11.

EDIT: la liste d’assemblage de Saw OP pour mon code. Je doute que ce soit même un bug général avec VS2011 maintenant. Cela peut simplement être un bogue spécial pour le code de l’OP. J’ai exécuté le code de l’OP tel quel avec Clang 3.2, GCC 4.6.2 et VS2010 et dans tous les cas, les différences maximales étaient de ~ 1%.

Il suffit de comstackr les sources avec les modifications appropriées à mon fichier ne.c et aux ne.c /O2 et /GL . Voici la source

 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());} 

et l’assemblage correspondant:

  ; Listing generated by Microsoft (R) Optimizing Comstackr 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 comstack 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 comstack 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 comstack 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 comstack 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() qui utilisait les < , > et || les opérateurs sont clairement plus chers. ne1() et ne3() qui utilisent respectivement les opérateurs == et != , sont terser et équivalent.

Visual Studio 2011 est en version bêta . Je considérerais cela comme un bug. Mes tests avec deux autres compilateurs, à savoir gcc 4.6.2 et clang 3.2 , avec le commutateur d'optimisation O2 donné exactement le même assemblage pour les trois tests (que j'avais) sur ma boîte Windows 7. Voici un résumé:

 $ cat ne.c #include  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() {} 

cède avec 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: 

et avec clang:

  .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 

Ma suggestion serait de classer ceci en tant que bogue avec Microsoft Connect .

Note: Je les ai compilé en tant que source C, car je ne pense pas que l'utilisation du compilateur C ++ correspondant apporterait des modifications significatives ici.

C’est une excellente question, mais je pense que vous avez été victime de l’parsing des dépendances du compilateur.

Le compilateur doit seulement effacer les bits élevés de eax une fois, et ils restnt clairs pour la deuxième version. La deuxième version devrait payer le prix à xor eax, eax sauf que l’parsing du compilateur a prouvé qu’elle avait été laissée vide par la première version.

La deuxième version est capable de “sortingcher” en tirant parti du travail effectué par le compilateur dans la première version.

Comment mesurez-vous les temps? Est-ce “(version un, suivie de la version deux) dans une boucle”, ou “(version un dans une boucle) suivi de (version deux dans une boucle)”?

Ne faites pas les deux tests dans le même programme (à la place, recomstackz pour chaque version), ou si vous le faites, testez à la fois “version A first” et “version B first” et voyez si


Illustration de la sortingche:

 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(); 

Si la durée du timer2 est inférieure à la durée du timer1 , nous ne concluons pas que la multiplication par 31 est plus rapide que la multiplication par 2. Au lieu de cela, nous nous rendons compte que le compilateur a effectué une parsing de sous-expression commune.

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

Et la seule chose prouvée est que la multiplication par 31 est plus rapide que le calcul common . Ce qui n’est guère surprenant – la multiplication est beaucoup plus rapide que sqrt et exp .