Pregunta operadores int! = y == cuando se compara con cero


Descubrí que! = Y == no son las formas más rápidas para probar cero o no cero.

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

Compilador: VC ++ 11 Indicadores de optimización: / O2 / GL / LTCG

Esta es la salida de ensamblaje para x86-32. Las segundas versiones de ambas comparaciones fueron ~ 12% más rápidas tanto en x86-32 como en x86-64. Sin embargo, en x86-64 las instrucciones eran idénticas (las primeras versiones se veían exactamente como las segundas versiones), pero las segundas versiones eran aún más rápidas.

  1. ¿Por qué el compilador no genera la versión más rápida en x86-32?
  2. ¿Por qué las segundas versiones son aún más rápidas en x86-64 cuando la salida del ensamblaje es idéntica?

EDITAR: He agregado código de evaluación comparativa. ZERO: 1544ms, 1358ms NON_ZERO: 1544ms, 1358ms http://pastebin.com/m7ZSUrcP o http://anonymouse.org/cgi-bin/anon-www.cgi/http://pastebin.com/m7ZSUrcP

Nota: Probablemente no sea conveniente ubicar estas funciones cuando se compilan en un único archivo fuente, porque main.asm es bastante grande. Tenía zero1, zero2, nonZero1, nonZero2 en un archivo fuente separado.

EDIT2: ¿Podría alguien con VC ++ 11 y VC ++ 2010 instalado ejecutar el código de evaluación comparativa y publicar los tiempos? De hecho, podría ser un error en VC ++ 11.


75
2018-05-31 17:50


origen


Respuestas:


EDITAR: vi la lista de montaje de OP para mi código. Dudo que esto sea siquiera una error general con VS2011 ahora. Esto puede ser simplemente un error de caso especial para el código de OP. Ejecuté el código de OP tal como está con clang 3.2, gcc 4.6.2 y VS2010 y en todos los casos el diferencias máximas estaban en ~ 1%.

Acabo de compilar las fuentes con las modificaciones adecuadas a mi ne.c archivo y el /O2 y /GL banderas. Aquí está la fuente

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

y la asamblea correspondiente:

    ; 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() que usó el <, > y || operadores es claramente más caro. ne1() y ne3() que usan el == y != operadores respectivamente, son más estrictos y equivalentes.

Visual Studio 2011 es en beta. Yo consideraría esto como un error. Mis pruebas con otros dos compiladores gcc 4.6.2 y clang 3.2, con el O2 El conmutador de optimización produjo el mismo ensamblaje exacto para las tres pruebas (que tuve) en mi caja de Windows 7. Aquí hay un resumen:

$ 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() {}

rendimientos con 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:

y con 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

Mi sugerencia sería presentar esto como un error con Microsoft Connect.

Nota: los compilé como fuente C, ya que no creo que usar el compilador C ++ correspondiente haga un cambio significativo aquí.


19
2018-05-31 18:10



Esta es una gran pregunta, pero creo que has sido víctima del análisis de dependencia del compilador.

El compilador solo tiene que borrar los bits altos de eax una vez, y permanecen claros para la segunda versión. La segunda versión tendría que pagar el precio de xor eax, eax excepto que el análisis del compilador demostró que la primera versión lo dejó libre.

La segunda versión es capaz de "hacer trampa" aprovechando el trabajo que el compilador hizo en la primera versión.

¿Cómo estás midiendo los tiempos? ¿Es "(versión uno, seguido de la versión dos) en un bucle" o "(versión uno en un bucle) seguido de (versión dos en un bucle)"?

No haga ambas pruebas en el mismo programa (en su lugar, vuelva a compilar para cada versión) o, si lo hace, pruebe primero la "versión A primero" y la "versión B primero" y vea si lo que ocurra primero es pagar una penalización.


Ilustración de la trampa:

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 timer2 la duración es menor que timer1 duración, no concluimos que multiplicar por 31 es más rápido que multiplicar por 2. En su lugar, nos damos cuenta de que el compilador realizó un análisis de subexpresión común, y el código se convirtió en:

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

Y lo único probado es que multiplicar por 31 es más rápido que computar common. Lo cual no es sorprendente en absoluto: la multiplicación es mucho más rápida que sqrt y exp.


121
2018-05-31 17:58