Quando ho scritto questa risposta, stavo solo guardando il titolo della domanda su <vs. <= in generale, non l'esempio specifico di una costante a < 901
contro a <= 900
. Molti compilatori riducono sempre l'entità delle costanti convertendo tra <
e <=
, ad esempio perché l'operando immediato x86 ha una codifica a 1 byte più corta per -128..127.
Per ARM e in particolare per AArch64, essere in grado di codificare come immediato dipende dalla possibilità di ruotare un campo stretto in qualsiasi posizione in una parola. Quindi cmp w0, #0x00f000
sarebbe codificabile, mentre cmp w0, #0x00effff
potrebbe non esserlo. Quindi la regola make-it-small per il confronto rispetto a una costante di compilazione non si applica sempre per AArch64.
<vs. <= in generale, anche per condizioni variabili di runtime
Nel linguaggio assembly della maggior parte delle macchine, un confronto per <=
ha lo stesso costo di un confronto per <
. Questo vale sia che si stia ramificando su di esso, che lo si booleanti per creare un numero intero 0/1, sia che si usi come predicato per un'operazione di selezione senza rami (come x86 CMOV). Le altre risposte hanno affrontato solo questa parte della domanda.
Ma questa domanda riguarda gli operatori C ++, l' input per l'ottimizzatore. Normalmente sono entrambi ugualmente efficienti; i consigli del libro sembrano totalmente falsi perché i compilatori possono sempre trasformare il confronto che implementano in asm. Ma c'è almeno un'eccezione in cui l'utilizzo <=
può creare accidentalmente qualcosa che il compilatore non può ottimizzare.
Come condizione di loop, ci sono casi in cui <=
è qualitativamente diverso da <
, quando impedisce al compilatore di dimostrare che un loop non è infinito. Questo può fare una grande differenza, disabilitando l'auto-vettorializzazione.
L'overflow senza segno è ben definito come avvolgimento base-2, a differenza dell'overflow con segno (UB). I contatori di loop firmati sono generalmente al sicuro da ciò con i compilatori che si ottimizzano in base al superamento dell'UB non firmato: ++i <= size
alla fine diventeranno sempre falsi. ( Ciò che ogni programmatore C dovrebbe sapere sul comportamento indefinito )
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++)
...
I compilatori possono ottimizzare solo in modo da preservare il comportamento (definito e legalmente osservabile) della sorgente C ++ per tutti i possibili valori di input , ad eccezione di quelli che portano a comportamenti indefiniti.
(Un semplice i <= size
creerebbe anche il problema, ma ho pensato che calcolare un limite superiore fosse un esempio più realistico dell'introduzione accidentale della possibilità di un ciclo infinito per un input che non ti interessa ma che il compilatore deve considerare.)
In questo caso, size=0
porta a upper_bound=UINT_MAX
ed i <= UINT_MAX
è sempre vero. Quindi questo ciclo è infinito size=0
e il compilatore deve rispettarlo, anche se probabilmente il programmatore non intende mai passare size = 0. Se il compilatore può incorporare questa funzione in un chiamante in cui può dimostrare che size = 0 è impossibile, quindi ottimo, può ottimizzare come potrebbe i < size
.
Assomiglia if(!size) skip the loop;
do{...}while(--size);
è un modo normalmente efficiente per ottimizzare un for( i<size )
loop, se il valore effettivo di i
non è necessario all'interno del loop ( Perché i loop sono sempre compilati nello stile "do ... while" (tail jump)? ).
Ma ciò {} mentre non può essere infinito: se inserito con size==0
, otteniamo 2 ^ n iterazioni. (L' iterazione su tutti i numeri interi senza segno in un ciclo for C rende possibile esprimere un ciclo su tutti i numeri interi senza segno incluso lo zero, ma non è facile senza un flag carry come è in asm.)
Con una possibilità avvolgente del loop loop, i compilatori moderni spesso "rinunciano" e non ottimizzano in modo altrettanto aggressivo.
Esempio: somma di numeri interi da 1 a n
Usando le i <= n
sconfitte non firmate il riconoscimento del linguaggio di clang che ottimizza i sum(1 .. n)
loop con una forma chiusa basata sulla n * (n+1) / 2
formula di Gauss .
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
x86-64 asm da clang7.0 e gcc8.2 sull'esploratore del compilatore 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
Ma per la versione ingenua, abbiamo appena ottenuto un loop stupido da 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 non usa una forma chiusa in entrambi i modi, quindi la scelta della condizione del loop non la danneggia davvero ; si auto-vettorializza con l'aggiunta di numeri interi SIMD, eseguendo 4 i
valori in parallelo negli elementi di 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.
Ha anche un semplice ciclo scalare che penso che usi per n
casi di loop molto piccoli e / o infiniti.
A proposito, entrambi questi loop sprecano un'istruzione (e un up sulle CPU della famiglia Sandybridge) sull'overhead del loop. sub eax,1
/ jnz
invece di add eax,1
/ cmp / jcc sarebbe più efficiente. 1 uop invece di 2 (dopo macrofusione di sub / jcc o cmp / jcc). Il codice dopo entrambi i loop scrive EAX incondizionatamente, quindi non utilizza il valore finale del contatore loop.