Warum wird eine einfache Schleife optimiert, wenn das Limit 959, aber nicht 960 ist?

Lesezeit: 3 Minuten

Benutzeravatar von Graffe
Graff

Betrachten Sie diese einfache Schleife:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Kompiliert man mit gcc 7 (snapshot) oder clang (trunk) mit -march=core-avx2 -Ofast Sie erhalten 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, es setzt die Antwort ohne Schleife einfach auf 960.

Wenn Sie jedoch den Code ä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 durch? clang gibt zum Beispiel:

.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 gleich?


Das Limit für die gleiche Schleife, wenn Sie ersetzen float mit double ist 479. Dies gilt wieder für gcc und clang.

Aktualisierung 1

Es stellt sich heraus, dass sich gcc 7 (snapshot) und clang (trunk) sehr unterschiedlich verhalten. Clang optimiert die Loops für alle Limits unter 960, soweit ich das beurteilen kann. gcc hingegen ist empfindlich gegenüber dem genauen Wert und hat keine Obergrenze. Zum Beispiel es nicht Optimieren Sie die Schleife, wenn das Limit 200 (sowie viele andere Werte) ist, aber es tut wenn die Grenze 202 und 20002 (sowie viele andere Werte) ist.

  • Was Sulthan wahrscheinlich meint, ist, dass 1) der Compiler die Schleife entrollt und 2) sobald sie entrollt ist, sieht, dass die Summenoperationen zu einer gruppiert werden können. Wenn die Schleife nicht ausgerollt wird, können die Operationen nicht gruppiert werden.

    – Jean-Francois Fabre

    10. Februar 2017 um 12:39 Uhr


  • Eine ungerade Anzahl von Schleifen macht das Abrollen komplizierter, die letzten Iterationen müssen speziell durchgeführt werden. Das könnte durchaus ausreichen, um den Optimierer in einen Modus zu versetzen, in dem er die Verknüpfung nicht mehr erkennen kann. Es ist ziemlich wahrscheinlich, dass es den Code für den Spezialfall zuerst hinzufügen und dann wieder entfernen müsste. Den Optimierer zwischen den Ohren zu verwenden ist immer am besten 🙂

    – Hans Passant

    10. Februar 2017 um 13:02 Uhr


  • @HansPassant Es ist auch für jede Zahl kleiner als 959 optimiert.

    – Graff

    10. Februar 2017 um 13:05 Uhr

  • Würde dies nicht normalerweise mit der Eliminierung der Induktionsvariablen erfolgen, anstatt eine wahnsinnige Menge abzurollen? Das Abrollen um den Faktor 959 ist verrückt.

    – Harald

    10. Februar 2017 um 13:06 Uhr

  • @eleanora Ich habe mit diesem Compilre-Explorer gespielt und das Folgende scheint zu gelten (nur über den gcc-Snapshot zu sprechen): Wenn die Schleifenanzahl ein Vielfaches von 4 und mindestens 72 ist, dann ist die Schleife nicht abgerollt (oder besser gesagt um den Faktor 4 abgerollt); Andernfalls wird die gesamte Schleife durch eine Konstante ersetzt – auch wenn die Schleifenanzahl 2000000001 beträgt. Meine Vermutung: vorzeitige Optimierung (wie in einem verfrühten “Hey, ein Vielfaches von 4, das ist gut zum Ausrollen”, das eine weitere Optimierung blockiert, im Vergleich zu einem gründlicheren “Was hat es mit dieser Schleife überhaupt auf sich?”)

    – Hagen von Eitzen

    11. Februar 2017 um 22:10 Uhr

Benutzeravatar von Grzegorz Szpetkowski
Grzegorz Szpetkowski

TL;DR

Standardmäßig verhält sich der aktuelle Snapshot GCC 7 inkonsistent, während frühere Versionen aufgrund von Standardlimits eingeschränkt sind PARAM_MAX_COMPLETELY_PEEL_TIMESdas ist 16. Es kann von der Befehlszeile aus überschrieben werden.

Der Grund für die Begrenzung besteht darin, ein zu aggressives Abrollen der Schleife zu verhindern, was ein zweischneidiges Schwert sein kann.

GCC-Version <= 6.3.0

Die relevante Optimierungsoption für GCC ist -fpeel-loopsdas indirekt zusammen mit flag aktiviert wird -Ofast (Hervorhebung von mir):

Schält Loops, für die genügend Informationen vorliegen, dass sie nicht viel rollen (aus Profil-Feedback bzw statische Analyse). Es schaltet auch das vollständige Schleifen-Peeling ein (dh vollständige Entfernung von Schleifen mit kleiner konstanter Anzahl von Iterationen).

Aktiviert mit -O3 und/oder -fprofile-use.

Weitere Details erhalten Sie durch Hinzufügen -fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

Die Nachricht ist von /gcc/tree-ssa-loop-ivcanon.c:

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

somit try_peel_loop Funktion zurück false.

Eine ausführlichere Ausgabe kann mit erreicht werden -fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

Es ist möglich, die Grenzen zu optimieren, indem Sie mit klicken max-completely-peeled-insns=n und max-completely-peel-times=n Parameter:

max-completely-peeled-insns

Die maximale Anzahl von Insns einer vollständig abgezogenen Schleife.

max-completely-peel-times

Die maximale Anzahl von Iterationen einer Schleife, die für ein vollständiges Peeling geeignet ist.

Um mehr über insns zu erfahren, können Sie auf verweisen GCC Internes Handbuch.

Zum Beispiel, wenn Sie mit folgenden Optionen kompilieren:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

dann verwandelt sich Code in:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

Klirren

Ich bin mir nicht sicher, was Clang tatsächlich tut und wie man seine Grenzen optimiert, aber wie ich beobachtet habe, könnten Sie es zwingen, den endgültigen Wert auszuwerten, indem Sie die Schleife mit markieren Pragma ausrollenund es wird es vollständig entfernen:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

ergibt sich zu:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

  • Vielen Dank für diese sehr nette Antwort. Wie andere angemerkt haben, scheint gcc empfindlich auf die genaue Grenzgröße zu reagieren. Beispielsweise wird die Schleife für 912 nicht eliminiert godbolt.org/g/EQJHvT . Was sagt fdump-tree-cunroll-details in diesem Fall?

    – Graff

    10. Februar 2017 um 21:41 Uhr


  • Tatsächlich hat sogar 200 dieses Problem. Dies ist alles in einem Schnappschuss von gcc 7, den Godbolt bereitstellt. godbolt.org/g/Vg3SVs Dies gilt überhaupt nicht für Clang.

    – Graff

    10. Februar 2017 um 21:56 Uhr


  • Sie erklären die Mechanik des Peelings, aber nicht, was die Relevanz von 960 ist oder warum es überhaupt eine Grenze gibt

    – MM

    10. Februar 2017 um 23:06 Uhr

  • @MM: Das Peeling-Verhalten ist zwischen GCC 6.3.0 und dem neuesten Snaphost völlig unterschiedlich. Bei ersterem vermute ich stark, dass das fest codierte Limit durchgesetzt wird PARAM_MAX_COMPLETELY_PEEL_TIMES param, das ist definiert in /gcc/params.def:321 mit dem Wert 16.

    – Grzegorz Szpetkowski

    10. Februar 2017 um 23:16 Uhr

  • Vielleicht möchten Sie erwähnen warum GCC beschränkt sich bewusst auf diese Weise. Insbesondere wenn Sie Ihre Schleifen zu aggressiv entrollen, wird die Binärdatei größer und Sie passen weniger wahrscheinlich in den L1-Cache. Cache-Misses sind potenziell ziemlich teuer relativ zum Speichern einiger bedingter Sprünge unter der Annahme einer guten Verzweigungsvorhersage (die Sie für eine typische Schleife haben werden).

    – Kevin

    11. Februar 2017 um 5:58 Uhr


Benutzeravatar von Jean-François Fabre
Jean-Francois Fabre

Nachdem ich Sulthans Kommentar gelesen habe, denke ich, dass:

  1. Der Compiler rollt die Schleife vollständig aus, wenn der Schleifenzähler konstant (und nicht zu hoch) ist.

  2. Sobald es entrollt ist, sieht der Compiler, dass die Summenoperationen zu einer gruppiert werden können.

Wenn die Schleife aus irgendeinem Grund nicht ausgerollt wird (hier: es würde zu viele Anweisungen mit generieren 1000), können die Vorgänge nicht gruppiert werden.

Der Compiler könnte Beachten Sie, dass das Aufrollen von 1000 Anweisungen auf eine einzelne Addition hinausläuft, aber die oben beschriebenen Schritte 1 und 2 sind zwei separate Optimierungen, sodass Sie nicht das “Risiko” des Aufrollens eingehen können, ohne zu wissen, ob die Operationen gruppiert werden können (Beispiel: ein Funktionsaufruf kann nicht gruppiert werden).

Hinweis: Dies ist ein Sonderfall: Wer verwendet eine Schleife, um dasselbe noch einmal hinzuzufügen? Verlassen Sie sich in diesem Fall nicht auf das mögliche Entrollen/Optimieren des Compilers; Schreiben Sie die richtige Operation direkt in eine Anweisung.

  • dann kannst du dich darauf konzentrieren not too high Teil? Ich meine, warum ist das Risiko in dem Fall nicht da 100 ? Ich habe etwas vermutet … in meinem Kommentar oben … kann es der Grund dafür sein?

    – Benutzer2736738

    10. Februar 2017 um 12:49 Uhr

  • Ich denke, dass der Compiler sich der Ungenauigkeit der Gleitkommazahl, die er auslösen könnte, nicht bewusst ist. Ich denke, es ist nur eine Begrenzung der Befehlsgröße. Du hast max-unrolled-insns neben max-unrolled-times

    – Jean-Francois Fabre

    10. Februar 2017 um 13:03 Uhr


  • Ah, es war irgendwie mein Gedanke oder meine Vermutung … ich möchte eine klarere Argumentation bekommen.

    – Benutzer2736738

    10. Februar 2017 um 13:06 Uhr

  • Interessanterweise ändert man das float zu einem intist der gcc-Compiler aufgrund seiner Induktionsvariablenoptimierungen in der Lage, die Schleife unabhängig von der Anzahl der Iterationen zu reduzieren (-fivopts). Aber die scheinen nicht zu funktionieren floats.

    – Tavian Barnes

    10. Februar 2017 um 17:29 Uhr

  • @CortAmmon Richtig, und ich erinnere mich, dass ich einige Leute gelesen habe, die überrascht und verärgert waren, dass GCC MPFR verwendet, um sehr große Zahlen genau zu berechnen, was ziemlich andere Ergebnisse liefert als die entsprechenden Gleitkommaoperationen, die Fehler und Genauigkeitsverlust angesammelt hätten. Zeigt, dass viele Leute Gleitkommazahlen falsch berechnen.

    – Zan Luchs

    10. Februar 2017 um 22:31 Uhr

Sehr gute Frage!

Sie scheinen eine Grenze für die Anzahl der Iterationen oder Operationen erreicht zu haben, die der Compiler beim Vereinfachen des Codes einzubetten versucht. Wie von Grzegorz Szpetkowski dokumentiert, gibt es Compiler-spezifische Möglichkeiten, diese Grenzen mit Pragmas oder Befehlszeilenoptionen zu optimieren.

Sie können auch mit spielen Godbolts Compiler Explorer um zu vergleichen, wie sich verschiedene Compiler und Optionen auf den generierten Code auswirken: gcc 6.2 und icc 17 immer noch den Code für 960 inline, während clang 3.9 nicht (mit der standardmäßigen Godbolt-Konfiguration stoppt das Inlining tatsächlich bei 73).

  • Ich habe die Frage bearbeitet, um die Versionen von gcc und clang zu verdeutlichen, die ich verwendet habe. Sehen godbolt.org/g/FfwWjL . Ich verwende zum Beispiel -Ofast.

    – Graff

    10. Februar 2017 um 16:36 Uhr


1423290cookie-checkWarum wird eine einfache Schleife optimiert, wenn das Limit 959, aber nicht 960 ist?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy