java c++ operators micro-optimization premature-optimization

java - x>-1 vs x>=0, hay una diferencia de rendimiento



c++ operators (10)

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

Tu maestro está fundamentalmente equivocado. No solo por qué es probable que comparando con 0 puede ser extremadamente rápido, sino porque este tipo de optimización local está bien hecho por su compilador / intérprete, y puede meterse todo tratando de ayudar. Definitivamente no es algo bueno para enseñar.

Puedes leer: this o this

He escuchado a un profesor soltar esto una vez, y me ha estado molestando desde entonces. Digamos que queremos verificar si el entero x es mayor o igual que 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 que >= . En este caso, era Java, pero según él, esto también se aplicaba a C, c ++ y otros lenguajes. ¿Hay algo de verdad en esta afirmación?


"> =" es una operación única, 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).


De hecho, creo que la segunda versión debe 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 dichas llamadas.


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

En todo caso, esperaría que (x >= 0) fuera ligeramente más rápido, ya que la comparación con 0 es gratuita en algunos conjuntos de instrucciones (como ARM).

Por supuesto, cualquier compilador sensato elegirá la mejor implementación, independientemente de la variante que esté en su fuente.


En primer lugar, depende en gran medida de la plataforma de hardware. Para los PC modernos y los SoC de ARM la diferencia depende principalmente de las optimizaciones del compilador. Pero para las CPU sin FPU, las matemáticas firmadas serían un desastre.

Por ejemplo, las CPU simples de 8 bits, como Intel 8008, 8048,8051, Zilog Z80, Motorola 6800 o incluso los modernos microcontenedores RIC PIC o Atmel, hacen todas las operaciones matemáticas mediante ALU con registros de 8 bits y básicamente solo llevan bit de indicador y z (cero indicador de valor) bits de indicador. Todas las matemáticas serias se hacen a través de bibliotecas, y la expresión

BYTE x; if (x >= 0)

Definitivamente ganaría, usando las instrucciones JZ o JNZ asm sin costosas llamadas a la biblioteca.


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

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

  • Asumo una operación int firmada (que parece ser 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 GCC, /Ox para MSVC, -Oh para IAR)
  • usando 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 cada uno de ellos produjo para las operaciones de comparación:

MSVC 11 targeting 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 dirigida 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 dirigida 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 segmentación 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 segmentación 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 targeting 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 dirigida 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, estas son las diferencias de cualquier nota entre la evaluación (x > -1) y (x >= 0) que aparece:

  • MSVC targeting ARM usa cmp r0,#0xFFFFFFFF para (x > -1) vs cmp r0,#0 para (x >= 0) . El código de operación de la primera instrucción tiene dos bytes más. Supongo que puede introducir algo de tiempo adicional, por lo que llamaremos a esto una ventaja para (x >= 0)
  • La orientación de MSVC x86 usa cmp ecx, -1 para (x > -1) frente a test ecx, ecx para (x >= 0) . El código de operación de la primera instrucción es un byte más largo. Supongo que 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 códigos de máquina idénticos para los dos tipos de comparación (con la posible excepción de qué registro se utilizó). Entonces, de acuerdo con esta encuesta, parece que (x >= 0) tiene una muy pequeña posibilidad de ser ''más rápido''. Pero cualquiera que sea la ventaja que pueda tener la codificación de byte de código de operación mínimamente más corta (y me estresaría) se verá completamente eclipsada por otros factores.

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

En resumen, no te preocupes por esta micro-optimización. Creo que mi escritura aquí ya ha pasado más tiempo de lo que gastará cualquier diferencia en el rendimiento de estas expresiones acumuladas en todas las CPU que las ejecutan 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í.


Perdón por irrumpir en esta conversación sobre el rendimiento.

Antes de hacer una digresión, observemos que la JVM tiene instructions especiales para manejar no solo cero, sino también constantes de uno a tres. Dicho esto, es probable que la capacidad de la arquitectura para manejar cero se haya perdido mucho más allá de la optimización del compilador, sino también de la conversión de códigos de bytes a códigos de máquina y demás.

Recuerdo de mis días de lenguaje de ensamblador x86 que había instrucciones en el conjunto para mayor que ( ja ) y mayor que o igual a ( jae ). Harías uno de estos:

; x >= 0 mov ax, [x] mov bx, 0 cmp ax, bx jae above ; x > -1 mov ax, [x] mov bx, -1 cmp ax, bx ja above

Estas alternativas toman la misma cantidad de tiempo, porque las instrucciones son idénticas o similares, y consumen una cantidad predecible de ciclos de reloj. Ver, por ejemplo, this . ja y jae pueden verificar un número diferente de registros aritméticos, pero ese control está dominado por la necesidad de que la instrucción tome un tiempo predecible. Esto a su vez es necesario para mantener la arquitectura de la CPU manejable.

Pero vine aquí para hacer una digresión.

Las respuestas antes que yo tienden a ser pertinentes, y también indicativas de que vas a estar en el mismo estadio de béisbol en lo que respecta al rendimiento, independientemente del enfoque que elijas.

Lo cual te deja elegir según otros criterios. Y aquí es donde quería hacer una nota. Al probar los índices, prefiera la verificación de estilo ajustado estrechamente, principalmente x >= lowerBound , a la x > lowerBound - 1 . El argumento está destinado a ser inventado, pero se reduce a la legibilidad, ya que aquí todo lo demás es realmente igual.

Como conceptualmente estás probando contra un límite inferior, x >= lowerBound es la prueba canónica que obtiene la cognición más adecuada de los lectores de tu código. x + 10 > lowerBound + 9 , x - lowerBound >= 0 y x > -1 son todas formas indirectas de probar contra un límite inferior.

De nuevo, lamento irrumpir, pero sentí que esto era importante más allá de lo académico. Siempre pienso en estos términos y dejo que el compilador se preocupe por las optimizaciones mínimas que cree que puede salir de jugar con las constantes y el rigor de los operadores.


Sí, hay una diferencia, debería ver el bytecode.

para

if (x >= 0) { }

el bytecode es

ILOAD 1 IFLT L1

para

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 cero operandos

iflt : jump if less than zero

Pero es posible ver la diferencia solo ejecutando JVM en el 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)


Tu maestra ha estado leyendo algunos libros realmente viejos. Solía ​​ser el caso con algunas arquitecturas que carecen de la instrucción greater than or equal que la evaluación requiere menos ciclos de máquina que >= , pero estas plataformas son raras en estos días. Sugiero ir por la legibilidad, y usando >= 0 .


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

No debe considerar constantemente hacer optimizaciones minúsculas en su código a costa de la legibilidad, ya que hará que leer y mantener el código sea más difícil. Si estas optimizaciones deben tener lugar, resúmalas en funciones de nivel inferior, de modo que aún te queda un código que es más fácil de leer para los humanos.

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

Como nota al margen, si está usando C, quizás escribir una macro que use el código ligeramente más eficiente es una solución más factible, ya que logrará más eficiencia, legibilidad y facilidad de mantenimiento que las operaciones dispersas.

Y, por supuesto, las compensaciones de eficiencia y legibilidad dependen de su aplicación. Si ese bucle se ejecuta 10000 veces por segundo, entonces es posible que haya 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 de vez en cuando, probablemente no valga la pena para ganar minutos.