x > -1 frente a x >= 0, ¿hay alguna diferencia de rendimiento?

10 minutos de lectura

avatar de usuario
Quirón

Escuché a un maestro soltar esto una vez, y me ha estado molestando desde entonces. Digamos que queremos comprobar si el número entero x es mayor o igual a 0. Hay dos formas de verificar esto:

if (x > -1){
    //do stuff
}

y

if (x >= 0){
    //do stuff
} 

Según este maestro > sería un poco más rápido entonces >=. En este caso fue Java, pero según él esto también aplicaba para C, c++ y otros lenguajes. ¿Hay algo de verdad en esta afirmación?

  • Y el tipo de x es…?

    – Jon Skeet

    25 de enero de 2013 a las 11:26

  • … ‘el entero x’ ?

    – Grant Thomas

    25 de enero de 2013 a las 11:27


  • @Cheiron: Piensa en lo que esto significa si x es un uint escribe…

    – Jon Skeet

    25 de enero de 2013 a las 11:29

  • Las expresiones no tienen sentido con tipos sin signo: la primera nunca es verdadera y la segunda siempre es verdadera.

    – James Kanze

    25 de enero de 2013 a las 11:33

  • posible duplicado de Is

    – nawfal

    17/07/2014 a las 11:00

avatar de usuario
graham borland

Depende mucho de la arquitectura subyacente, pero cualquier diferencia será minúscula.

En todo caso, esperaría (x >= 0) ser un poco más rápido, en comparación con 0 viene gratis en algunos conjuntos de instrucciones (como ARM).

Por supuesto, cualquier compilador sensato elegirá la mejor implementación sin importar qué variante esté en su fuente.

  • +1. Es muy probable que el hecho de que 0 esté involucrado sea tan importante (o más) que la diferencia entre las dos operaciones de comparación (si las hay).

    – Thilo

    25 de enero de 2013 a las 11:29


  • @Thilo Eso es posiblemente cierto en algunas arquitecturas (en cuyo caso, esperaría que el compilador haga el cambio por sí mismo). En otros (como Intel), los dos son exactamente idénticos en el tiempo.

    – James Kanze

    25 de enero de 2013 a las 11:31

  • Editado para mencionar que los compiladores elegirán lo mejor de todos modos.

    –Graham Borland

    25 de enero de 2013 a las 11:33

  • Acordado; los programadores no deberían preocuparse por este nivel de detalle a menos que estén programando las arquitecturas.

    – Aram Kocharyan

    25 de enero de 2013 a las 11:43

  • Me gustaría agregar la razón por la que >= 0 sería más rápido que > -1. Esto se debe al montaje. siempre comparando con 0. Si el segundo valor no es 0, el primer valor sería sumado (o restado) por el segundo valor, después de esa posible comparación sería e, lt, le, gt, ge, ne (igual, menor que, menor o igual, mayor que, mayor o igual, no igual). Por supuesto, la adición/sustracción adicional requeriría ciclos de CPU adicionales.

    – Destrictor

    26 de enero de 2013 a las 1:09

No hay diferencia en ningún sentido del mundo real.

Echemos un vistazo a algunos códigos generados por varios compiladores para varios objetivos.

  • Estoy asumiendo una operación int firmada (que parece la intención del OP)
  • He limitado por encuesta a C y a los compiladores que tengo a mano (ciertamente, una muestra bastante pequeña: GCC, MSVC e IAR)
  • optimizaciones básicas habilitadas (-O2 para CCG, /Ox para MSVC, -Oh para IAR)
  • utilizando el siguiente módulo:

    void my_puts(char const* s);
    
    void cmp_gt(int x) 
    {
        if (x > -1) {
            my_puts("non-negative");
        }
        else {
            my_puts("negative");
        }
    }
    
    void cmp_gte(int x) 
    {
        if (x >= 0) {
            my_puts("non-negative");
        }
        else {
            my_puts("negative");
        }
    }
    

Y esto es lo que produjo cada uno de ellos para las operaciones de comparación:

MSVC 11 dirigido a ARM:

// if (x > -1) {...
00000        |cmp_gt| PROC
  00000 f1b0 3fff    cmp         r0,#0xFFFFFFFF
  00004 dd05         ble         |$LN2@cmp_gt|


// if (x >= 0) {...
  00024      |cmp_gte| PROC
  00024 2800         cmp         r0,#0
  00026 db05         blt         |$LN2@cmp_gte|

MSVC 11 dirigido a x64:

// if (x > -1) {...
cmp_gt  PROC
  00000 83 f9 ff     cmp     ecx, -1
  00003 48 8d 0d 00 00                  // speculative load of argument to my_puts()
    00 00        lea     rcx, OFFSET FLAT:$SG1359
  0000a 7f 07        jg  SHORT $LN5@cmp_gt

// if (x >= 0) {...
cmp_gte PROC
  00000 85 c9        test    ecx, ecx
  00002 48 8d 0d 00 00                  // speculative load of argument to my_puts()
    00 00        lea     rcx, OFFSET FLAT:$SG1367
  00009 79 07        jns     SHORT $LN5@cmp_gte

MSVC 11 dirigido a x86:

// if (x > -1) {...
_cmp_gt PROC
  00000 83 7c 24 04 ff   cmp     DWORD PTR _x$[esp-4], -1
  00005 7e 0d        jle     SHORT $LN2@cmp_gt


// if (x >= 0) {...
_cmp_gte PROC
  00000 83 7c 24 04 00   cmp     DWORD PTR _x$[esp-4], 0
  00005 7c 0d        jl  SHORT $LN2@cmp_gte

GCC 4.6.1 dirigido a x64

// if (x > -1) {...
cmp_gt:
    .seh_endprologue
    test    ecx, ecx
    js  .L2

// if (x >= 0) {...
cmp_gte:
    .seh_endprologue
    test    ecx, ecx
    js  .L5

GCC 4.6.1 dirigido a x86:

// if (x > -1) {...
_cmp_gt:
    mov eax, DWORD PTR [esp+4]
    test    eax, eax
    js  L2

// if (x >= 0) {...
_cmp_gte:
    mov edx, DWORD PTR [esp+4]
    test    edx, edx
    js  L5

GCC 4.4.1 dirigido a ARM:

// if (x > -1) {...
cmp_gt:
    .fnstart
.LFB0:
    cmp r0, #0
    blt .L8

// if (x >= 0) {...
cmp_gte:
    .fnstart
.LFB1:
    cmp r0, #0
    blt .L2

IAR 5.20 dirigido a un ARM Cortex-M3:

// if (x > -1) {...
cmp_gt:
80B5 PUSH     {R7,LR}
.... LDR.N    R1,??DataTable1  ;; `?<Constant "non-negative">`
0028 CMP      R0,#+0
01D4 BMI.N    ??cmp_gt_0

// if (x >= 0) {...
cmp_gte:
 80B5 PUSH     {R7,LR}
 .... LDR.N    R1,??DataTable1  ;; `?<Constant "non-negative">`
 0028 CMP      R0,#+0
 01D4 BMI.N    ??cmp_gte_0

Si todavía estás conmigo, aquí están las diferencias de cualquier nota entre evaluar (x > -1) y (x >= 0) que aparecen:

  • MSVC dirigido a usos de ARM cmp r0,#0xFFFFFFFF por (x > -1) contra cmp r0,#0 por (x >= 0). El código de operación de la primera instrucción es dos bytes más largo. Supongo que eso puede introducir algo de tiempo adicional, por lo que llamaremos a esto una ventaja para (x >= 0)
  • MSVC dirigido a usos x86 cmp ecx, -1 por (x > -1) contra test ecx, ecx por (x >= 0). El código de operación de la primera instrucción es un byte más largo. Supongo que eso puede introducir algo de tiempo adicional, por lo que llamaremos a esto una ventaja para (x >= 0)

Tenga en cuenta que GCC e IAR generaron un código de máquina idéntico para los dos tipos de comparación (con la posible excepción de qué registro se usó). De acuerdo con esta encuesta, parece que (x >= 0) tiene una posibilidad muy pequeña de ser ‘más rápido’. Pero cualquiera que sea la ventaja que pueda tener la codificación de bytes de código de operación mínimamente más corta (y recalco podría tener) será sin duda completamente eclipsado por otros factores.

Me sorprendería si encontrara algo diferente para la salida jitted de Java o C#. Dudo que encuentre alguna diferencia notable incluso para un objetivo muy pequeño como un AVR de 8 bits.

En resumen, no se preocupe por esta microoptimización. Creo que mi escrito aquí ya ha pasado más tiempo del que pasará por cualquier diferencia en el rendimiento de estas expresiones acumuladas en todas las CPU ejecutándolas en mi vida. Si tiene la capacidad de medir la diferencia en el rendimiento, aplique sus esfuerzos a algo más importante como estudiar el comportamiento de las partículas subatómicas o algo así.

  • ¿Y si justo antes de la comparación necesitas calcular x?…. Por ejemplo, el MUY común –x ?

    – qPCR4vir

    25 de enero de 2013 a las 22:45


  • No esperaría que eso tuviera un impacto significativo en la capacidad del compilador para generar código equivalente para el > -1 o >= 0 operaciones.

    – Michael Burr

    25 de enero de 2013 a las 23:07


  • Estos fragmentos de código no ilustran realmente el hecho de que el 0-la comparación es gratuita (al menos en ARM) si x ha sido calculado inmediatamente antes, mientras que el -1 la comparación requeriría una instrucción adicional explícita.

    –Graham Borland

    28 de enero de 2013 a las 9:45


  • @GrahamBorland: Tenga en cuenta que la mayoría de los ejemplos ARM aquí tratados x > -1 exactamente el mismo que x >= 0 (es decir, notaron que las expresiones son equivalentes). Esperaría que hicieran lo mismo si x fueron calculados – por el momento no tengo un sistema para probar esa suposición. Por otro lado, el compilador MSVC ARM los trata de manera ligeramente diferente y puedo probar el compilador MS ARM. Todavía realiza una comparación explícita para las pruebas -1 y 0 si x se calcula (todavía hay un cmp r3,#0 o cmp r3,#0xffffffff después de realizado el cálculo).

    – Michael Burr

    28 de enero de 2013 a las 10:45

  • @MichaelBurr, en realidad no me sorprende en absoluto que el compilador de MS no detecte esta optimización obvia. 🙂

    –Graham Borland

    28 de enero de 2013 a las 10:54

Tu maestro ha estado leyendo algunos libros muy antiguos. Solía ​​ser el caso con algunas arquitecturas que carecían de la greater than or equal instrucción que evaluar > requiere menos ciclos de máquina que >=, pero estas plataformas son raras en estos días. Sugiero buscar legibilidad y usar >= 0.

  • Pero digamos que tenemos una arquitectura que no es de PC como Arduino. ¿Habría alguna diferencia allí?

    – Quirón

    25 de enero de 2013 a las 11:35

  • @Cheiron: Y el compilador tiene un millón de años y no puede detectar la optimización.

    – Martín York

    25 de enero de 2013 a las 11:41

  • @Cheiron Incluso los AVR de 8 bits de ATMEL tienen la BRGE (rama si es mayor o igual) y BRSH (rama si es igual o superior) instrucciones, por lo que no verá ninguna diferencia.

    – Serguéi Kalinichenko

    25 de enero de 2013 a las 11:43


avatar de usuario
Aram Kocharyan

Una preocupación mayor aquí es optimización prematura. Muchos consideran escribir legible código más importante que escribir eficiente código [1, 2]. Aplicaría estas optimizaciones como última etapa en una biblioteca de bajo nivel una vez que se haya demostrado que el diseño funciona.

No debería considerar constantemente hacer optimizaciones minúsculas en su código a costa de la legibilidad, ya que dificultará la lectura y el mantenimiento del código. Si es necesario llevar a cabo estas optimizaciones, abstráigalas en funciones de nivel inferior para que aún le quede un código que sea más fácil de leer para los humanos.

Como un ejemplo loco, considere a alguien que escribe sus programas en ensamblador a alguien que está dispuesto a renunciar a esa eficiencia adicional y usar Java por sus beneficios en diseño, facilidad de uso y mantenibilidad.

Como nota al margen, si está usando C, tal vez escribir una macro que use el código un poco más eficiente sea una solución más factible, ya que logrará eficiencia, legibilidad y mantenibilidad más que operaciones dispersas.

Y, por supuesto, las ventajas y desventajas de la eficiencia y la legibilidad dependen de su aplicación. Si ese bucle se ejecuta 10000 veces por segundo, es posible que sea un cuello de botella y es posible que desee invertir tiempo en optimizarlo, pero si se trata de una declaración única que se llama ocasionalmente, probablemente no valga la pena por la ganancia de minutos.

avatar de usuario
Evgeniy Dorofeev

Sí, hay una diferencia, debería ver el código de bytes.

por

if (x >= 0) {}

el bytecode es

ILOAD 1
IFLT L1

por

if (x > -1) {}

el bytecode es

ILOAD 1
ICONST_M1
IF_ICMPLE L3

La versión 1 es más rápida porque usa una operación especial de operando cero

iflt : jump if less than zero 

Pero es posible ver la diferencia solo ejecutando JVM en modo de solo interpretación java -Xint ...por ejemplo, esta prueba

int n = 0;       
for (;;) {
    long t0 = System.currentTimeMillis();
    int j = 0;
    for (int i = 100000000; i >= n; i--) {
        j++;
    }
    System.out.println(System.currentTimeMillis() - t0);
}

muestra 690 ms para n = 0 y 760 ms para n = 1. (Usé 1 en lugar de -1 porque es más fácil de demostrar, la idea sigue siendo la misma)

  • ¿Ha activado las optimizaciones? ¿El JIT no lo optimizará?

    – Martín York

    25 de enero de 2013 a las 11:43


  • Wow, el maestro también se equivocó en “cuál es más rápido” 🙂

    – Serguéi Kalinichenko

    25 de enero de 2013 a las 11:47

  • for(int x = 10000000; x >= 0; x–) { }

    – gran chico

    25 de enero de 2013 a las 11:55


  • intente mi prueba con java -Xint Test, funciona y muestra alguna diferencia

    – Evgeniy Dorofeev

    25 de enero de 2013 a las 12:10

  • Por favor, repita la prueba codificando el 0 y el 1, pero no arroje la variable n.

    – qPCR4vir

    25 de enero de 2013 a las 19:06

avatar de usuario
Ivailo Strandjev

De hecho, creo que la segunda versión debería ser un poco más rápida, ya que requiere una verificación de un solo bit (suponiendo que compare en cero como se muestra arriba). Sin embargo, tales optimizaciones nunca se muestran realmente, ya que la mayoría de los compiladores optimizarán tales llamadas.

  • ¿Ha activado las optimizaciones? ¿El JIT no lo optimizará?

    – Martín York

    25 de enero de 2013 a las 11:43


  • Wow, el maestro también se equivocó en “cuál es más rápido” 🙂

    – Serguéi Kalinichenko

    25 de enero de 2013 a las 11:47

  • for(int x = 10000000; x >= 0; x–) { }

    – gran chico

    25 de enero de 2013 a las 11:55


  • intente mi prueba con java -Xint Test, funciona y muestra alguna diferencia

    – Evgeniy Dorofeev

    25 de enero de 2013 a las 12:10

  • Por favor, repita la prueba codificando el 0 y el 1, pero no arroje la variable n.

    – qPCR4vir

    25 de enero de 2013 a las 19:06

avatar de usuario
gran chico

“>=” es una sola operación, al igual que “>”. No 2 operaciones separadas con OR.

Pero >=0 es probablemente más rápido, porque la computadora necesita verificar solo un bit (signo negativo).

  • También habría que ver cómo x obtiene su valor (análisis de flujo de datos). Es posible que el compilador ya conozca el resultado sin verificar nada.

    – Bo Person

    25 de enero de 2013 a las 11:52

  • Si su compilador es tonto y no puede optimizar x > -1 en algo que la máquina puede hacer de manera eficiente, sí >= 0 puede ser más rápido en algunos ISA (como MIPS donde hay un bgez $reg, target instrucción que como dices bifurca en el bit de signo de un registro). Ser más rápido permite un diseño de hardware inteligente para los componentes internos de MIPS, pero no hace que la comparación sea más rápida para el software. Todas las instrucciones simples tienen latencia de 1 ciclo, ya sea or (bits independientes) o add.

    – Peter Cordes

    23 de diciembre de 2020 a las 2:30

¿Ha sido útil esta solución?