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.
<
es dos veces más rápido que escribir<=
.Respuestas:
No, no será más rápido en la mayoría de las arquitecturas. No especificó, pero en x86, todas las comparaciones integrales se implementarán generalmente en dos instrucciones de máquina:
test
ocmp
instrucción, que estableceEFLAGS
Jcc
instrucción (salto) , según el tipo de comparación (y el diseño del código):jne
- Salta si no es igual ->ZF = 0
jz
- Saltar si cero (igual) ->ZF = 1
jg
- Salta si es mayor ->ZF = 0 and SF = OF
Ejemplo (Editado por brevedad) Compilado con
$ gcc -m32 -S -masm=intel test.c
Compila a:
Y
Compila a:
Entonces, la única diferencia entre los dos es
jg
unajge
instrucción versus una . Los dos tomarán la misma cantidad de tiempo.Me gustaría abordar el comentario de que nada indica que las diferentes instrucciones de salto toman la misma cantidad de tiempo. Esto es un poco complicado de responder, pero esto es lo que puedo dar: en la Referencia del conjunto de instrucciones de Intel , todos están agrupados bajo una instrucción común,
Jcc
(Saltar si se cumple la condición). La misma agrupación se realiza bajo el Manual de referencia de optimización , en el Apéndice C. Latencia y rendimiento.Los valores para
Jcc
son:con la siguiente nota al pie sobre
Jcc
:Por lo tanto, nada en los documentos de Intel trata una
Jcc
instrucción de manera diferente a las demás.Si se piensa en la circuitería real utilizada para implementar las instrucciones, se puede suponer que habría puertas AND / OR simples en los diferentes bits
EFLAGS
para determinar si se cumplen las condiciones. Entonces, no hay razón para que una instrucción que prueba dos bits tome 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: (Más o menos el mismo código que el anterior, pero con en
double
lugar deint
).fuente
jg
yjnle
son la misma instrucción,7F
:-)Históricamente (estamos hablando de la década de 1980 y principios de 1990), hubo algunas arquitecturas en las que esto era cierto. El problema raíz es que la comparación de enteros se implementa inherentemente a través de sustracciones de enteros. Esto da lugar a los siguientes casos.
Ahora, cuando
A < B
la resta tiene que pedir prestado un bit alto para que la resta sea correcta, al igual que llevas y pides prestado al sumar y restar a mano. Este bit "prestado" generalmente se denominaba bit de acarreo y sería comprobable 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 implicaba igualdad.Por lo general, había al menos dos instrucciones de bifurcación condicionales, una para bifurcar en el bit de acarreo y otra en el bit cero.
Ahora, para llegar al meollo del asunto, expandamos la tabla anterior para incluir los resultados de acarreo y cero bits.
Por lo tanto, la implementación de una rama para
A < B
se puede hacer en una instrucción, porque el bit de acarreo es claro solo en este caso, es decir,Pero, si queremos hacer una comparación menor o igual, necesitamos hacer una verificación adicional de la bandera de cero para detectar el caso de la igualdad.
Entonces, en algunas máquinas, el uso de una comparación "menor que" podría ahorrar una instrucción de máquina . Esto fue relevante en la era de la velocidad del procesador por debajo de los megahercios y las relaciones de velocidad de CPU a memoria 1: 1, pero hoy en día es casi totalmente irrelevante.
fuente
jge
, que prueban tanto el cero como las banderas de signo / acarreo.<=
prueba puede ser implementado en una instrucción con el canje de los operandos y pruebas paranot <
(a equivalentes>=
) Esta es la deseada<=
con operandos intercambiadas:cmp B,A; bcs addr
. Esa es la razón por la que Intel omitió esta prueba, la consideraron redundante y no podía permitirse instrucciones redundantes en esos momentos :-)Suponiendo que estamos hablando de tipos enteros internos, no hay forma posible de que uno sea más rápido que el otro. Obviamente son semánticamente idénticos. Ambos le piden al compilador que haga exactamente lo mismo. Solo un compilador horriblemente roto generaría un código inferior para uno de estos.
Si hubiera alguna plataforma donde
<
fuera más rápido que<=
para los tipos enteros simples, el compilador siempre debería convertir<=
a<
constantes. Cualquier compilador que no sea solo sería un mal compilador (para esa plataforma).fuente
<
tampoco<=
tienen velocidad hasta que el compilador decida qué velocidad tendrán. Esta es una optimización muy simple para los compiladores cuando considera que generalmente ya realizan una optimización de código muerto, optimización de llamada de cola, elevación de bucle (y desenrollado, en ocasiones), paralelización automática de varios bucles, etc. ¿ Por qué perder el tiempo reflexionando sobre optimizaciones prematuras? ? Obtenga un prototipo en funcionamiento, perfílelo para determinar dónde se encuentran las optimizaciones más significativas, realice esas optimizaciones en orden de importancia y vuelva a perfilar en el camino para medir el progreso ...(a < C)
a(a <= C-1)
(para alguna constanteC
) haceC
que sea más difícil codificar en el conjunto de instrucciones. Por ejemplo, un conjunto de instrucciones puede ser capaz de representar constantes con signo de -127 a 128 en una forma compacta en las comparaciones, pero las constantes fuera de ese rango tienen que cargarse utilizando una codificación más larga y más lenta u otra instrucción por completo. Por lo tanto, una comparación como(a < -127)
puede no tener una transformación directa.a > 127
aa > 128
porque no tiene otra opción existe, se utiliza el que necesita. Estamos comparandoa > 127
aa >= 128
, que no puede requerir codificación diferente o diferentes instrucciones, ya que tienen la misma tabla de verdad. Cualquier codificación de uno es igualmente una codificación del otro.<=
a<
constantes". Hasta donde yo sé, esa transformación implica cambiar la constante. Por ejemplo,a <= 42
se compila comoa < 43
porque<
es más rápido. En algunos casos extremos, tal transformación no sería fructífera porque la nueva constante puede requerir instrucciones más o más lentas. Por supuestoa > 127
, ya >= 128
son equivalentes y un compilador debe codificar ambas formas en la (misma) manera más rápida, pero eso no es inconsistente con lo que he dicho.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.
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, se genera el mismo código de máquina en cualquier caso.
fuente
if(a <=900)
para demostrar que genera exactamente lo mismo :)Para el código de coma flotante, la comparación <= puede ser más lenta (según una instrucción) incluso en arquitecturas modernas. Aquí está la primera función:
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, coloca el bit "comparado menos que" en su lugar y luego regresa. Se necesitan cuatro instrucciones.Ahora considere esta función en su lugar:
Esto requiere el mismo trabajo que
compare_strict
antes, pero ahora hay dos partes de interés: "era menor que" y "era igual a". Esto requiere una instrucción adicional (cror
- registro de condición a nivel de bit O) para combinar estos dos bits en uno. Entoncescompare_loose
requiere cinco instrucciones, mientras quecompare_strict
requiere cuatro.Puede pensar que el compilador podría optimizar la segunda función de esta manera:
Sin embargo, esto manejará incorrectamente los NaN.
NaN1 <= NaN2
yNaN1 > NaN2
necesitan ambos evaluar a falso.fuente
fucomip
establece ZF y CF.cr
es el equivalente a banderas comoZF
yCF
en el x86. (Aunque el CR es más flexible). De lo que habla el afiche es de mover el resultado a un GPR: que toma dos instrucciones en PowerPC, pero x86 tiene una instrucción de movimiento condicional.Tal vez el autor de ese libro sin nombre ha leído que se
a > 0
ejecuta más rápidoa >= 1
y cree que es cierto universalmente.Pero es porque a
0
está involucrado (porqueCMP
puede, dependiendo de la arquitectura, reemplazado, por ejemplo, porOR
) y no por el<
.fuente
(a >= 1)
ir más despacio que(a > 0)
, ya que el primero puede ser trivialmente transformaron a este último por el optimizador ..Por lo menos, si esto fuera cierto, un compilador podría optimizar trivialmente a <= b to! (A> b), por lo que incluso si la comparación en sí fuera más lenta, con todos los compiladores menos ingenuos, no notaría una diferencia .
fuente
NOT
está hecho por otra instrucción (je
vs.jne
)Tienen la misma velocidad. Tal vez en alguna arquitectura especial lo que él / ella dijo es correcto, pero en la familia x86 al menos sé que son lo mismo. Porque para hacer esto, la CPU hará una sustracción (a - b) y luego verificará las banderas del registro de banderas. Dos bits de ese registro se llaman ZF (indicador cero) y SF (indicador de signo), y se realiza en un ciclo, porque lo hará con una operación de máscara.
fuente
Esto dependería mucho de la arquitectura subyacente con la que se compila el C. Algunos procesadores y arquitecturas pueden tener instrucciones explícitas para igual o menor que, que se ejecutan en diferentes números de ciclos.
Sin embargo, eso sería bastante inusual, ya que el compilador podría solucionarlo, haciéndolo irrelevante.
fuente
TL; respuesta DR
Para la mayoría de las combinaciones de arquitectura, compilador y lenguaje, no será más rápido.
Respuesta completa
Otras respuestas se han concentrado en la arquitectura x86 , y no conozco la arquitectura ARM (que parece ser su ensamblador de ejemplo) lo suficientemente bien como para comentar específicamente sobre el código generado, pero este es un ejemplo de una micro-optimización que es muy arquitectónica. específico, y es tan probable que sea una anti-optimización como lo es ser una optimización .
Como tal, sugeriría que este tipo de micro-optimización es un ejemplo de programación de culto de carga en lugar de la mejor práctica de ingeniería de software.
Probablemente hay algunas arquitecturas donde esto es una optimización, pero sé de al menos una arquitectura donde lo contrario puede ser cierto. La venerable arquitectura Transputer solo tenía instrucciones de código de máquina para igual y mayor o igual que , por lo que todas las comparaciones tuvieron que construirse a partir de estas primitivas.
Incluso entonces, en casi todos los casos, el compilador podría ordenar las instrucciones de evaluación de tal manera que, en la práctica, ninguna comparación tuviera ninguna ventaja sobre ninguna otra. Sin embargo, en el peor de los casos, es posible que deba agregar una instrucción inversa (REV) para intercambiar los dos primeros elementos en la pila de operandos . Esta fue una instrucción de un solo byte que tardó un solo ciclo en ejecutarse, por lo que tenía la sobrecarga más pequeña posible.
Si una micro-optimización como esta es una optimización o una anti-optimización depende de la arquitectura específica que esté utilizando, por lo general, es una mala idea adquirir el hábito de usar micro-optimizaciones específicas de la arquitectura, de lo contrario podría instintivamente use uno cuando no sea apropiado hacerlo, y parece que esto es exactamente lo que defiende el libro que está leyendo.
fuente
No debería ser capaz de notar la diferencia, incluso si hay alguna. Además, en la práctica, tendrá que hacer un adicional
a + 1
oa - 1
hacer que la condición se mantenga a menos que vaya a usar algunas constantes mágicas, lo cual es una práctica muy mala por todos los medios.fuente
Se podría decir que la línea es correcta en la mayoría de los lenguajes de secuencias de comandos, ya que el carácter adicional resulta en un procesamiento de código ligeramente más lento. Sin embargo, como señaló la respuesta principal, no debería tener efecto en C ++, y cualquier cosa que se haga con un lenguaje de script probablemente no esté tan preocupado por la optimización.
fuente
Cuando escribí esta respuesta, sólo estaba mirando a la pregunta del título sobre vs. <<= en general, no el ejemplo específico de una constante
a < 901
vsa <= 900
. Muchos compiladores siempre reducen la magnitud de las constantes al convertir entre<
y<=
, por ejemplo, porque el operando inmediato x86 tiene una codificación más corta de 1 byte para -128..127.Para ARM y especialmente AArch64, poder codificar como inmediato depende de poder rotar un campo estrecho en cualquier posición de una palabra. Entonces
cmp w0, #0x00f000
sería codificable, mientras quecmp w0, #0x00effff
podría no serlo. Por lo tanto, la regla de hacer más pequeño para la comparación frente a una constante de tiempo de compilación no siempre se aplica a AArch64.<vs. <= en general, incluso para condiciones variables de tiempo de ejecución
En lenguaje ensamblador en la mayoría de las máquinas, una comparación
<=
tiene el mismo costo que una comparación para<
. Esto se aplica ya sea que se bifurque en él, lo booleanice para crear un entero 0/1 o lo use como predicado para una operación de selección sin ramificación (como x86 CMOV). Las otras respuestas solo han abordado esta parte de la pregunta.Pero esta pregunta es sobre los operadores de C ++, la entrada al optimizador. Normalmente ambos son igualmente eficientes; el consejo del libro suena totalmente falso porque los compiladores siempre pueden transformar la comparación que implementan en asm. Pero hay al menos una excepción en la que el uso
<=
puede crear accidentalmente algo que el compilador no puede optimizar.Como condición de bucle, hay casos en los que
<=
es cualitativamente diferente de<
, cuando impide que el compilador pruebe que un bucle no es infinito. Esto puede hacer una gran diferencia, deshabilitando la auto-vectorización.El desbordamiento sin signo está bien definido como envoltura de base 2, a diferencia del desbordamiento con signo (UB). Los contadores de bucles firmados generalmente están a salvo de esto con compiladores que se optimizan en función de que el UB de desbordamiento firmado no suceda:
++i <= size
siempre se convertirá en falso. ( Lo que todo programador de C debe saber sobre el comportamiento indefinido )Los compiladores solo pueden optimizar de manera que preserven el comportamiento (definido y legalmente observable) de la fuente C ++ para todos los valores de entrada posibles , excepto los que conducen a un comportamiento indefinido.
(Un simple
i <= size
también crearía el problema, pero pensé que calcular un límite superior era un ejemplo más realista de introducir accidentalmente la posibilidad de un bucle infinito para una entrada que no le interesa pero que el compilador debe considerar).En este caso,
size=0
conduce aupper_bound=UINT_MAX
, yi <= UINT_MAX
siempre es cierto. Por lo tanto, este bucle es infinitosize=0
y el compilador debe respetarlo aunque usted, como programador, probablemente nunca tenga la intención de pasar size = 0. Si el compilador puede incorporar esta función a una persona que llama, donde puede demostrar que size = 0 es imposible, entonces es genial, puede optimizar como podríai < size
.Asm like
if(!size) skip the loop;
do{...}while(--size);
es una forma normalmente eficiente de optimizar unfor( i<size )
bucle, si el valor real dei
no es necesario dentro del bucle ( ¿Por qué los bucles siempre se compilan en el estilo "do ... while" (salto de cola)? ).Pero eso sí {} mientras que no puede ser infinito: si se ingresa con
size==0
, obtenemos 2 ^ n iteraciones. ( Iterar sobre todos los enteros sin signo en un bucle for C hace posible expresar un bucle sobre todos los enteros sin signo, incluido cero, pero no es fácil sin un indicador de acarreo de la forma en que está en asm).Dado que el contador de bucle es una posibilidad, los compiladores modernos a menudo simplemente "se dan por vencidos" y no optimizan tan agresivamente.
Ejemplo: suma de enteros de 1 a n
El uso de las
i <= n
derrotas sin signo de reconocimiento de idioma clang que optimiza lossum(1 .. n)
bucles con una forma cerrada basada en lan * (n+1) / 2
fórmula de Gauss .Asistente x86-64 de clang7.0 y gcc8.2 en el explorador del compilador Godbolt
Pero para la versión ingenua, solo obtenemos un bobo tonto de clang.
GCC no usa una forma cerrada de ninguna manera, por lo que la elección de la condición del bucle realmente no lo perjudica ; se auto-vectoriza con la suma de enteros SIMD, ejecutando 4
i
valores en paralelo en los elementos de un registro XMM.También tiene un bucle escalar simple que creo que usa para
n
casos muy pequeños y / o para el caso de bucle infinito.Por cierto, ambos bucles desperdician una instrucción (y una uop en las CPU de la familia Sandybridge) en la sobrecarga del bucle.
sub eax,1
/ enjnz
lugar deadd eax,1
/ cmp / jcc sería más eficiente. 1 uop en lugar de 2 (después de la macro fusión de sub / jcc o cmp / jcc). El código después de ambos bucles escribe EAX incondicionalmente, por lo que no está utilizando el valor final del contador de bucles.fuente
<
o<=
. Pero claro,test ecx,ecx
/bt eax, 3
/jbe
saltará si ZF está configurado (ecx == 0), o si CF está configurado (bit 3 de EAX == 1), causando un bloqueo parcial de bandera en la mayoría de las CPU porque las banderas que lee no todas provienen de la última instrucción para escribir cualquier bandera. En la familia Sandybridge, en realidad no se detiene, solo necesita insertar una fusión.cmp
/test
escribe todas las banderas, perobt
deja ZF sin modificar. felixcloutier.com/x86/btSolo si las personas que crearon las computadoras son malas con la lógica booleana. Que no deberían ser.
Cada comparación (
>=
<=
>
<
) se puede hacer a la misma velocidad.Lo que cada comparación es, es solo una resta (la diferencia) y ver si es positiva / negativa.
(Si
msb
se establece, el número es negativo)¿Cómo verificar
a >= b
? Suba-b >= 0
Check sia-b
es positivo.¿Cómo verificar
a <= b
? Sub0 <= b-a
Check sib-a
es positivo.¿Cómo verificar
a < b
? Suba-b < 0
Check sia-b
es negativo.¿Cómo verificar
a > b
? Sub0 > b-a
Check sib-a
es negativo.En pocas palabras, la computadora puede hacer esto debajo del capó para la operación dada:
a >= b
==msb(a-b)==0
a <= b
==msb(b-a)==0
a > b
==msb(b-a)==1
a < b
==msb(a-b)==1
y por supuesto el equipo no sería realmente necesita hacer la
==0
o==1
tampoco.porque
==0
podría simplemente invertir elmsb
del circuito.De todos modos, ciertamente no se habrían
a >= b
calculado comoa>b || a==b
jajajafuente