Als ich diese Antwort schrieb, war ich auf der Suche nur auf der Titel - Frage zu <vs. <= in der Regel nicht das spezifische Beispiel eines konstanten a < 901
vs. a <= 900
. Viele Compiler verkleinern die Größe von Konstanten immer durch Konvertieren zwischen <
und <=
, z. B. weil der x86-Sofortoperand eine kürzere 1-Byte-Codierung für -128..127 hat.
Für ARM und insbesondere AArch64 hängt die Fähigkeit, sofort zu codieren, davon ab, dass ein schmales Feld in eine beliebige Position in einem Wort gedreht werden kann. Also cmp w0, #0x00f000
wäre kodierbar, cmp w0, #0x00effff
könnte aber nicht sein. Daher gilt die AA-Regel zum Vergleich mit einer Konstante zur Kompilierungszeit nicht immer für AArch64.
<vs. <= im Allgemeinen, auch für Bedingungen mit Laufzeitvariablen
In der Assemblersprache der meisten Maschinen hat ein Vergleich für <=
die gleichen Kosten wie ein Vergleich für <
. Dies gilt unabhängig davon, ob Sie darauf verzweigen, es boolesch machen, um eine 0/1-Ganzzahl zu erstellen, oder es als Prädikat für eine verzweigungslose Auswahloperation (wie x86 CMOV) verwenden. Die anderen Antworten haben nur diesen Teil der Frage angesprochen.
Bei dieser Frage geht es jedoch um die C ++ - Operatoren, die Eingabe in den Optimierer. Normalerweise sind beide gleich effizient. Der Rat aus dem Buch klingt völlig falsch, da Compiler den Vergleich, den sie in asm implementieren, immer transformieren können. Es gibt jedoch mindestens eine Ausnahme, bei der die Verwendung <=
versehentlich etwas erzeugen kann, das der Compiler nicht optimieren kann.
Als Schleifenbedingung gibt es Fälle, in denen <=
sich der Compiler qualitativ davon unterscheidet <
, zu beweisen, dass eine Schleife nicht unendlich ist. Dies kann einen großen Unterschied machen und die automatische Vektorisierung deaktivieren.
Der vorzeichenlose Überlauf ist im Gegensatz zum vorzeichenbehafteten Überlauf (UB) als Base-2-Wrap-Around gut definiert. Vorzeichenbehaftete Schleifenzähler sind im Allgemeinen davor sicher, da Compiler, die basierend auf dem nicht auftretenden UB mit vorzeichenbehaftetem Überlauf optimieren, nicht ++i <= size
irgendwann falsch werden. ( Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte )
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++)
...
Compiler können nur so optimieren, dass das (definierte und rechtlich beobachtbare) Verhalten der C ++ - Quelle für alle möglichen Eingabewerte erhalten bleibt , mit Ausnahme derjenigen, die zu undefiniertem Verhalten führen.
(Ein einfaches i <= size
würde auch das Problem verursachen, aber ich dachte, die Berechnung einer Obergrenze wäre ein realistischeres Beispiel für die versehentliche Einführung der Möglichkeit einer Endlosschleife für eine Eingabe, die Sie nicht interessieren, die der Compiler jedoch berücksichtigen muss.)
In diesem Fall size=0
führt upper_bound=UINT_MAX
und i <= UINT_MAX
ist immer wahr. Diese Schleife ist also unendlich für size=0
, und der Compiler muss dies respektieren, obwohl Sie als Programmierer wahrscheinlich nie beabsichtigen, size = 0 zu übergeben. Wenn der Compiler diese Funktion in einen Aufrufer einbinden kann, in dem er beweisen kann, dass size = 0 unmöglich ist, kann er großartig optimieren, wie es für möglich wäre i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
ist eine normalerweise effiziente Methode zur Optimierung einer for( i<size )
Schleife, wenn der tatsächliche Wert von i
innerhalb der Schleife nicht benötigt wird ( Warum werden Schleifen immer im Stil "do ... while" kompiliert (Tail Jump)? ).
Aber das kann nicht unendlich sein: Wenn size==0
wir mit eingeben, erhalten wir 2 ^ n Iterationen. (Das Iterieren über alle vorzeichenlosen Ganzzahlen in einer for-Schleife C ermöglicht es, eine Schleife über alle vorzeichenlosen Ganzzahlen einschließlich Null auszudrücken, aber ohne ein Übertragsflag ist es nicht einfach, wie es in asm ist.)
Da der Umlauf des Schleifenzählers möglich ist, geben moderne Compiler oft nur auf und optimieren nicht annähernd so aggressiv.
Beispiel: Summe von ganzen Zahlen von 1 bis n
Verwenden von vorzeichenlosen i <= n
Niederlagen Clangs Redewendung, die sum(1 .. n)
Schleifen mit einer geschlossenen Form basierend auf der Gaußschen n * (n+1) / 2
Formel optimiert .
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 von clang7.0 und gcc8.2 im Godbolt-Compiler-Explorer
# 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
Aber für die naive Version bekommen wir nur eine dumme Schleife von 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 verwendet in keiner Weise eine geschlossene Form, so dass die Wahl der Schleifenbedingung nicht wirklich schadet . Es wird automatisch mit einer SIMD-Ganzzahladdition vektorisiert, wobei 4 i
Werte parallel in den Elementen eines XMM-Registers ausgeführt werden.
# "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.
Es hat auch eine einfache Skalarschleife, die meiner Meinung nach für sehr kleine n
und / oder für den Endlosschleifenfall verwendet wird.
Übrigens verschwenden diese beiden Schleifen einen Befehl (und einen UOP auf CPUs der Sandybridge-Familie) für den Schleifen-Overhead. sub eax,1
/ jnz
anstelle von add eax,1
/ cmp / jcc wäre effizienter. 1 uop statt 2 (nach Makrofusion von sub / jcc oder cmp / jcc). Der Code nach beiden Schleifen schreibt EAX bedingungslos, sodass nicht der Endwert des Schleifenzählers verwendet wird.