¿Es <más rápido que <=?
¿ Es if (a < 901)
más rápido que if (a <= 900)
?
No exactamente como en este ejemplo simple, pero hay ligeros cambios de rendimiento en el código complejo de bucle. Supongo que esto tiene que ver con el código de máquina generado, en caso de que sea cierto.
No, no será más rápido en la mayoría de las arquitecturas. No lo especificaste, pero en x86, todas las comparaciones integrales generalmente se implementarán en dos instrucciones de máquina:
- Una
test
ocmp
instrucción, que estableceEFLAGS
- Y una
Jcc
instrucción (de salto) , según el tipo de comparación (y el diseño del código): jne
- Saltar si no es igual -->ZF = 0
jz
- Saltar si es cero (igual) -->ZF = 1
jg
- Saltar si es mayor -->ZF = 0 and SF = OF
- (etc...)
Ejemplo (Editado por brevedad) Compilado con$ gcc -m32 -S -masm=intel test.c
if (a < b) {
// Do something 1
}
Se compila en:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jge .L2 ; jump if a is >= b
; Do something 1
.L2:
Y
if (a <= b) {
// Do something 2
}
Se compila en:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jg .L5 ; jump if a is > b
; Do something 2
.L5:
Entonces, la única diferencia entre los dos es una instrucción jg
versus una . jge
Los dos tardarán la misma cantidad de tiempo.
Me gustaría abordar el comentario de que nada indica que las diferentes instrucciones de salto tomen la misma cantidad de tiempo. Esta es un poco complicada de responder, pero esto es lo que puedo dar: en la Referencia del conjunto de instrucciones Intel , todas están agrupadas bajo una instrucción común Jcc
(salte si se cumple la condición). La misma agrupación se realiza en el Manual de referencia de optimización , en el Apéndice C. Latencia y rendimiento.
Latencia : la cantidad de ciclos de reloj necesarios para que el núcleo de ejecución complete la ejecución de todos los μops que forman una instrucción.
Rendimiento : la cantidad de ciclos de reloj necesarios que se deben esperar antes de que los puertos de emisión estén libres para aceptar la misma instrucción nuevamente. Para muchas instrucciones, el rendimiento de una instrucción puede ser significativamente menor que su latencia.
Los valores para Jcc
son:
Latency Throughput
Jcc N/A 0.5
con la siguiente nota a pie de página sobre Jcc
:
- La selección de instrucciones de salto condicional debe basarse en la recomendación de la sección 3.4.1, “Optimización de la predicción de ramas”, para mejorar la previsibilidad de las ramas. Cuando las ramas se predicen con éxito, la latencia
jcc
es efectivamente cero.
Por lo tanto, nada en los documentos de Intel trata una Jcc
instrucción de manera diferente a las demás.
Si uno piensa en los circuitos reales utilizados para implementar las instrucciones, puede suponer que habría puertas Y/O simples en los diferentes bits de EFLAGS
, para determinar si se cumplen las condiciones. Entonces, no hay razón para que una instrucción que prueba dos bits deba tomar más o menos tiempo que una que prueba solo uno (ignorando el retraso de propagación de la puerta, que es mucho menor que el período del reloj).
Editar: punto flotante
Esto también es válido para el punto flotante x87: (prácticamente el mismo código que el anterior, pero con double
en lugar de int
.)
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
fstp st(0)
seta al ; Set al if above (CF=0 and ZF=0).
test al, al
je .L2
; Do something 1
.L2:
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; (same thing as above)
fstp st(0)
setae al ; Set al if above or equal (CF=0).
test al, al
je .L5
; Do something 2
.L5:
leave
ret
Históricamente (estamos hablando de los años 1980 y principios de los 1990), hubo algunas arquitecturas en las que esto era cierto. El problema fundamental es que la comparación de números enteros se implementa inherentemente mediante restas de números enteros. Esto da lugar a los siguientes casos.
Comparison Subtraction
---------- -----------
A < B --> A - B < 0
A = B --> A - B = 0
A > B --> A - B > 0
Ahora, cuando A < B
la resta tiene que pedir prestado un bit alto para que la resta sea correcta, al igual que llevas y tomas prestado cuando sumas y restas a mano. Este bit "prestado" generalmente se denominaba bit de acarreo y se podía probar mediante una instrucción de bifurcación. Un segundo bit llamado bit cero se establecería si la resta fuera idénticamente cero, lo que implica igualdad.
Por lo general, había al menos dos instrucciones de bifurcación condicionales, una para bifurcarse en el bit de acarreo y otra en el bit cero.
Ahora, para llegar al meollo del asunto, ampliemos la tabla anterior para incluir los resultados de acarreo y bit cero.
Comparison Subtraction Carry Bit Zero Bit
---------- ----------- --------- --------
A < B --> A - B < 0 0 0
A = B --> A - B = 0 1 1
A > B --> A - B > 0 1 0
Entonces, implementar una rama para A < B
se puede hacer en una instrucción, porque el bit de acarreo está claro solo en este caso, es decir,
;; Implementation of "if (A < B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; Branch if Carry is Zero to the new address
Pero, si queremos hacer una comparación menor o igual, debemos hacer una verificación adicional del indicador cero para detectar el caso de igualdad.
;; Implementation of "if (A <= B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; branch if A < B
bzs address ;; also, Branch if the Zero bit is Set
Entonces, en algunas máquinas, usar una comparación "menor que" podría ahorrar una instrucción de máquina . Esto era relevante en la era de la velocidad del procesador por debajo de los megahercios y las relaciones de velocidad de CPU a memoria de 1:1, pero hoy es casi totalmente irrelevante.
Suponiendo que estamos hablando de tipos de enteros internos, no hay forma posible de que uno pueda ser más rápido que el otro. Obviamente son semánticamente idénticos. Ambos le piden al compilador que haga exactamente lo mismo. Sólo un compilador terriblemente defectuoso generaría un código inferior para uno de estos.
Si hubiera alguna plataforma en la que <
fuera más rápido que <=
los tipos enteros simples, el compilador siempre debería convertir <=
a <
constantes. Cualquier compilador que no lo hiciera sería simplemente un mal compilador (para esa plataforma).
Veo que ninguno es más rápido. El compilador genera el mismo código de máquina en cada condición con un valor diferente.
if(a < 901)
cmpl $900, -4(%rbp)
jg .L2
if(a <=901)
cmpl $901, -4(%rbp)
jg .L3
Mi ejemplo if
es de GCC en la plataforma x86_64 en Linux.
Los escritores de compiladores son personas bastante inteligentes y piensan en estas cosas y en muchas otras que la mayoría de nosotros damos por sentado.
Noté que si no es una constante, entonces se genera el mismo código de máquina en cualquier caso.
int b;
if(a < b)
cmpl -4(%rbp), %eax
jge .L2
if(a <=b)
cmpl -4(%rbp), %eax
jg .L3
Para el código de punto flotante, la comparación <= puede ser más lenta (por una instrucción) incluso en arquitecturas modernas. Aquí está la primera función:
int compare_strict(double a, double b) { return a < b; }
En PowerPC, primero se realiza una comparación de punto flotante (que actualiza cr
el registro de condición), luego mueve el registro de condición a un GPR, desplaza el bit "comparado menos que" a su lugar y luego regresa. Se necesitan cuatro instrucciones.
Ahora considere esta función en su lugar:
int compare_loose(double a, double b) { return a <= b; }
Esto requiere el mismo trabajo que compare_strict
el anterior, pero ahora hay dos puntos de interés: "era menor que" y "era igual a". Esto requiere una instrucción adicional ( cror
- registro de condición bit a bit OR) para combinar estos dos bits en uno. Por eso compare_loose
requiere cinco instrucciones, mientras que compare_strict
requiere cuatro.
Se podría pensar que el compilador podría optimizar la segunda función de esta manera:
int compare_loose(double a, double b) { return ! (a > b); }
Sin embargo, esto manejará incorrectamente los NaN. NaN1 <= NaN2
y NaN1 > NaN2
es necesario evaluar ambos como falso.