Betrachten Sie diese einfache Schleife:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 959; i++)
p += 1;
return p;
}
Wenn Sie mit gcc 7 (Snapshot) oder clang (Trunk) kompilieren, erhalten -march=core-avx2 -Ofast
Sie etwas sehr Ähnliches.
.LCPI0_0:
.long 1148190720 # float 960
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Mit anderen Worten, die Antwort wird ohne Schleife auf 960 gesetzt.
Wenn Sie den Code jedoch ändern in:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 960; i++)
p += 1;
return p;
}
Die produzierte Baugruppe führt tatsächlich die Schleifensumme aus? Zum Beispiel gibt clang:
.LCPI0_0:
.long 1065353216 # float 1
.LCPI0_1:
.long 1086324736 # float 6
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
vxorps ymm1, ymm1, ymm1
mov eax, 960
vbroadcastss ymm2, dword ptr [rip + .LCPI0_1]
vxorps ymm3, ymm3, ymm3
vxorps ymm4, ymm4, ymm4
.LBB0_1: # =>This Inner Loop Header: Depth=1
vaddps ymm0, ymm0, ymm2
vaddps ymm1, ymm1, ymm2
vaddps ymm3, ymm3, ymm2
vaddps ymm4, ymm4, ymm2
add eax, -192
jne .LBB0_1
vaddps ymm0, ymm1, ymm0
vaddps ymm0, ymm3, ymm0
vaddps ymm0, ymm4, ymm0
vextractf128 xmm1, ymm0, 1
vaddps ymm0, ymm0, ymm1
vpermilpd xmm1, xmm0, 1 # xmm1 = xmm0[1,0]
vaddps ymm0, ymm0, ymm1
vhaddps ymm0, ymm0, ymm0
vzeroupper
ret
Warum ist das so und warum ist es für clang und gcc genau das gleiche?
Die Grenze für die gleiche Schleife , wenn Sie ersetzen float
mit double
ist 479. Dies ist das gleiche wieder für gcc und Klirren ist.
Update 1
Es stellt sich heraus, dass sich gcc 7 (Schnappschuss) und clang (Trunk) sehr unterschiedlich verhalten. clang optimiert die Loops für alle Limits unter 960, soweit ich das beurteilen kann. gcc hingegen reagiert empfindlich auf den genauen Wert und hat keine Obergrenze. Zum Beispiel es nicht optimiert die Schleife, wenn die Grenze 200 (wie auch viele andere Werte), aber es tut , wenn die Grenze 202 und 20002 (sowie viele andere Werte) ist.
quelle
Antworten:
TL; DR
Standardmäßig verhält sich der aktuelle Snapshot GCC 7 inkonsistent, während frühere Versionen aufgrund von
PARAM_MAX_COMPLETELY_PEEL_TIMES
16 ein Standardlimit haben. Es kann über die Befehlszeile überschrieben werden.Das Grundprinzip des Limits besteht darin, ein zu aggressives Abrollen der Schleife zu verhindern, das ein zweischneidiges Schwert sein kann .
GCC-Version <= 6.3.0
Die relevante Optimierungsoption für GCC ist
-fpeel-loops
, die indirekt zusammen mit dem Flag aktiviert wird-Ofast
(Schwerpunkt liegt bei mir):Weitere Details erhalten Sie durch Hinzufügen von
-fdump-tree-cunroll
:Die Nachricht stammt von
/gcc/tree-ssa-loop-ivcanon.c
:daher
try_peel_loop
kehrt die Funktion zurückfalse
.Eine ausführlichere Ausgabe kann erreicht werden mit
-fdump-tree-cunroll-details
:Es ist möglich, die Grenzen durch Plaing mit
max-completely-peeled-insns=n
undmax-completely-peel-times=n
params zu optimieren :Weitere Informationen zu Insns finden Sie im GCC Internals Manual .
Zum Beispiel, wenn Sie mit folgenden Optionen kompilieren:
dann verwandelt sich Code in:
Clang
Ich bin nicht sicher, was Clang tatsächlich tut und wie man seine Grenzen ändert, aber wie ich beobachtet habe, könnten Sie es zwingen, den Endwert zu bewerten, indem Sie die Schleife mit einem Abroll-Pragma markieren , und es wird sie vollständig entfernen:
Ergebnisse in:
quelle
PARAM_MAX_COMPLETELY_PEEL_TIMES
param erzwungen wird , das/gcc/params.def:321
mit dem Wert 16 definiert ist.Nachdem ich Sulthans Kommentar gelesen habe, denke ich, dass:
Der Compiler rollt die Schleife vollständig ab, wenn der Schleifenzähler konstant (und nicht zu hoch) ist.
Sobald es abgewickelt ist, sieht der Compiler, dass die Summenoperationen zu einer zusammengefasst werden können.
Wenn die Schleife aus irgendeinem Grund nicht abgewickelt wird (hier: es würden zu viele Anweisungen mit generiert
1000
), können die Operationen nicht gruppiert werden.Der Compiler konnte feststellen, dass das Abrollen von 1000 Anweisungen eine einzelne Addition darstellt. Die oben beschriebenen Schritte 1 und 2 sind jedoch zwei separate Optimierungen, sodass er nicht das "Risiko" des Abrollens eingehen kann, ohne zu wissen, ob die Vorgänge gruppiert werden können (Beispiel: Ein Funktionsaufruf kann nicht gruppiert werden.
Hinweis: Dies ist ein Eckfall: Wer verwendet eine Schleife, um dasselbe erneut hinzuzufügen? Verlassen Sie sich in diesem Fall nicht darauf, dass der Compiler möglicherweise abrollt / optimiert. Schreiben Sie direkt die richtige Operation in eine Anweisung.
quelle
not too high
Teil konzentrieren? Ich meine, warum ist das Risiko bei nicht vorhanden100
? Ich habe etwas erraten ... in meinem Kommentar oben ... kann es der Grund dafür sein?max-unrolled-insns
nebenmax-unrolled-times
float
in einen ändern . Aber die scheinen nicht für s zu funktionieren .int
-fivopts
float
Sehr gute Frage!
Sie scheinen die Anzahl der Iterationen oder Operationen, die der Compiler bei der Vereinfachung des Codes inline zu setzen versucht, begrenzt zu haben. Wie von Grzegorz Szpetkowski dokumentiert, gibt es compilerspezifische Möglichkeiten, diese Grenzwerte mit Pragmas oder Befehlszeilenoptionen zu optimieren.
Sie können auch mit dem Compiler-Explorer von Godbolt spielen , um zu vergleichen, wie sich verschiedene Compiler und Optionen auf den generierten Code auswirken:
gcc 6.2
undicc 17
den Code für 960 weiterhin einbinden, währendclang 3.9
dies nicht der Fall ist (bei der Standardkonfiguration von Godbolt wird das Inlining bei 73 tatsächlich beendet).quelle