c++ - relacionales - operadores int!=y== cuando se compara con cero
operadores relacionales en c++ (2)
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.
- ¿Por qué el compilador no genera la versión más rápida en x86-32?
- ¿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.
EDITAR: vi la lista de montaje de OP para mi código. Dudo que esto sea un 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 las diferencias máximas fueron de ~ 1%.
Recién compilé las fuentes con las modificaciones adecuadas en mi archivo ne.c
y las banderas /O2
y /GL
. 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ó <
, >
y ||
operadores es claramente más caro. ne1()
y ne3()
que usan los operadores ==
y !=
respectivamente, son más estrictos y equivalentes.
Visual Studio 2011 está en beta . Yo consideraría esto como un error. Mis pruebas con otros dos compiladores, a saber, gcc 4.6.2 y clang 3.2 , con el conmutador de optimización O2
produjeron exactamente el mismo ensamblaje 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í.
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 se mantienen claros para la segunda versión. La segunda versión tendría que pagar el precio a 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 la duración del timer2
es menor que la duración del timer1
, 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 en common
. Lo cual no es sorprendente en absoluto: la multiplicación es mucho más rápida que sqrt
y exp
.