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
vs a <= 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 que cmp 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 )
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
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 a upper_bound=UINT_MAX
, y i <= UINT_MAX
siempre es cierto. Por lo tanto, este bucle es infinito size=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ía i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
es una forma normalmente eficiente de optimizar un for( i<size )
bucle, si el valor real de i
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 los sum(1 .. n)
bucles con una forma cerrada basada en la n * (n+1) / 2
fórmula de Gauss .
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
Asistente x86-64 de clang7.0 y gcc8.2 en el explorador del compilador Godbolt
# clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret
Pero para la versión ingenua, solo obtenemos un bobo tonto de clang.
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret
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.
# "naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something.
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
/ en jnz
lugar de add 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.