¿Es <más rápido que <=?

Resuelto Vinícius asked hace 12 años • 15 respuestas

¿ 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.

Vinícius avatar Aug 27 '12 09:08 Vinícius
Aceptado

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 testo cmpinstrucción, que estableceEFLAGS
  • Y una Jccinstrucció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 jgversus una . jgeLos 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 Jccson:

      Latency   Throughput
Jcc     N/A        0.5

con la siguiente nota a pie de página sobre Jcc:

  1. 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 jcces efectivamente cero.

Por lo tanto, nada en los documentos de Intel trata una Jccinstrucció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 doubleen 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
Jonathon Reinhart avatar Aug 27 '2012 02:08 Jonathon Reinhart

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 < Bla 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 < Bse 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.

Lucas avatar Aug 27 '2012 17:08 Lucas

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).

David Schwartz avatar Aug 27 '2012 02:08 David Schwartz

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 ifes 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
Adrian Cornish avatar Aug 27 '2012 02:08 Adrian Cornish

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 crel 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_strictel 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_looserequiere cinco instrucciones, mientras que compare_strictrequiere 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 <= NaN2y NaN1 > NaN2es necesario evaluar ambos como falso.

ridiculous_fish avatar Aug 27 '2012 18:08 ridiculous_fish