Verwendet der Compiler SSE-Anweisungen für einen normalen C-Code?

Lesezeit: 7 Minuten

Benutzeravatar von Jennifer M
Jennifer M.

Ich sehe Leute benutzen -msse -msse2 -mfpmath=sse Flags standardmäßig in der Hoffnung, dass dies die Leistung verbessert. Ich weiß, dass SSE aktiv wird, wenn spezielle Vektortypen im C-Code verwendet werden. Aber machen diese Flags einen Unterschied für normalen C-Code? Verwendet der Compiler SSE, um regulären C-Code zu optimieren?

  • Alle x86-64 (auch bekannt als AMD64) Architekturen haben SSE und SSE2. Wenn Sie also für ein x86-64-Ziel kompilieren, verwendet der Compiler SSE-Register und SSE- und SSE2-Erweiterungen. Im Allgemeinen sind C-Compiler ziemlich schlecht darin, Code zu vektorisieren, daher geben die Compiler meistens nicht vektorisierten SSE/SSE2-Code für “normales C” auf x86-64 aus. (Ich habe dies als Kommentar und nicht als Antwort geschrieben, weil ich nicht genau weiß, was Sie fragen.)

    – Nominelles Tier

    10. Juni 2018 um 17:40 Uhr

  • Ich frage: Ist es vernünftig, diese Flags an clang oder gcc zu liefern und zu erwarten, dass der Code, der nicht in Bezug auf vektorisierte Typen geschrieben ist, an Leistung gewinnt? Verwendet der Compiler in diesem Fall wirklich SSE-Anweisungen, um etwas zu vektorisieren, oder nicht?

    – Jennifer M.

    10. Juni 2018 um 18:09 Uhr

  • Dies hängt von der Zielarchitektur und den anderen verwendeten Flags ab. Sie helfen definitiv bei der 32-Bit-Intel-Architektur (x86) (der Code kann dann auf jedem x86-64-Prozessor (wenn 32-Bit-Code vom Betriebssystem/Kernel unterstützt wird) oder jedem x86-Prozessor mit SSE2-Unterstützung ausgeführt werden). Für x86-64-Ziele sind sie überflüssig (diese Flags sind standardmäßig in Kraft, basierend auf dem Ziel). Wenn der Compiler weiß, dass SSE/SSE2 verfügbar ist (basierend auf der Architektur oder von Options-Flags), wird er sein Bestes versuchen, sogar “normales C” zu vektorisieren; Sie sind im Allgemeinen einfach nicht sehr gut darin.

    – Nominelles Tier

    10. Juni 2018 um 18:58 Uhr


Benutzeravatar von Peter Cordes
Peter Kordes

Ja, moderne Compiler vektorisieren automatisch mit SSE2, wenn Sie mit vollständiger Optimierung kompilieren. Clang vektorisiert Schleifen bei -O2gcc bei -O3.

(GCC12 ermöglicht die Vektorisierung bei -O2 aber nur wenn es “sehr günstig” ist, mit -O3 immer noch erforderlich, um die meisten Schleifen mit laufzeitvariablen Fahrtzählungen zu vektorisieren.)

Sogar bei -O1 oder -Osverwenden Compiler SIMD-Lade-/Speicheranweisungen für Strukturen kopieren oder initialisieren oder andere Objekte, die breiter als ein Integer-Register sind. Das zählt nicht wirklich als Autovektorisierung; Es ist eher ein Teil ihrer standardmäßig integrierten Memset / Memcpy-Strategie für kleine Blöcke mit fester Größe. (Ohne -fno-builtingilt dies auch für die ausdrückliche Verwendung von memcpy mit kleinen konstanten Längen.) Es nutzt SIMD-Anweisungen und erfordert, dass sie vom Kernel unterstützt und aktiviert werden, unabhängig davon, ob Sie es “Vektorisierung” nennen oder nicht. (Kernel verwenden -mgeneral-regs-onlyoder in älteren GCC -mno-mmx -mno-sse um dies zu deaktivieren.)


SSE2 ist Basis / nicht optional für x86-64, sodass Compiler immer SSE1/SSE2-Anweisungen verwenden können, wenn sie auf x86-64 abzielen. Spätere Befehlssätze (SSE4, AVX, AVX2, AVX512 und Nicht-SIMD-Erweiterungen wie BMI2, popcnt usw.) müssen manuell aktiviert werden (z -march=x86-64-v3 oder -msse4.1), um dem Compiler mitzuteilen, dass es in Ordnung ist, Code zu erstellen, der nicht auf älteren CPUs ausgeführt werden kann. Oder es dazu zu bringen, mehrere Codeversionen zu generieren und zur Laufzeit auszuwählen, aber das hat zusätzlichen Overhead und lohnt sich nur für größere Funktionen.

-msse -msse2 -mfpmath=sse ist bereits der Standard für x86-64, aber nicht für 32-Bit-i386. Einige 32-Bit-Aufrufkonventionen geben FP-Werte in x87-Registern zurück, sodass es unpraktisch sein kann, SSE/SSE2 für die Berechnung zu verwenden und das Ergebnis dann speichern/neu laden zu müssen, um es in x87 zu erhalten st(0). Mit -mfpmath=sseverwenden intelligentere Compiler möglicherweise immer noch x87 für eine Berechnung, die einen FP-Rückgabewert erzeugt.

Auf 32-Bit-x86, -msse2 möglicherweise nicht standardmäßig aktiviert, es hängt davon ab, wie Ihr Compiler konfiguriert wurde. Wenn Sie 32-Bit verwenden, weil Sie auf CPUs abzielen, die so alt sind, dass sie kippen Wenn Sie 64-Bit-Code ausführen, sollten Sie sicherstellen, dass er deaktiviert ist oder nur -msse.

Der beste Weg, eine Binärdatei zu erstellen, die auf die CPU abgestimmt ist, auf der Sie kompilieren, ist -O3 -march=native -mfpmath=sseund verwenden Sie Link-Time-Optimierung + profilgeführte Optimierung. (ggf -fprofile-generate / mit einigen Testdaten ausführen / gcc -fprofile-use).

Verwenden -march=native erstellt Binärdateien, die möglicherweise nicht auf früheren CPUs ausgeführt werden, wenn der Compiler neue Anweisungen verwendet. Die profilgeführte Optimierung ist sehr hilfreich für gcc: Es entrollt Schleifen niemals ohne sie. Aber mit PGO weiß es, welche Schleifen oft / für viele Iterationen ausgeführt werden, dh welche Schleifen “heiß” sind und es wert sind, mehr Codegröße auszugeben. Link-Time-Optimierung ermöglicht Inlining / Constant-Propagation über Dateien hinweg. Es ist sehr hilfreich, wenn Sie C++ mit vielen kleinen Funktionen haben, die Sie eigentlich nicht in Header-Dateien definieren.


Siehe So entfernen Sie “Rauschen” aus der GCC/Clang-Assembly-Ausgabe? für mehr darüber, wie man sich die Compiler-Ausgabe ansieht und Sinn daraus macht.

Hier sind einige konkrete Beispiele im Godbolt-Compiler-Explorer für x86-64. Godbolt hat auch gcc für mehrere andere Architekturen, und mit clang können Sie hinzufügen -target mips oder was auch immer, sodass Sie auch die automatische Vektorisierung für ARM NEON mit den richtigen Compileroptionen sehen können, um sie zu aktivieren. Sie können verwenden -m32 mit den x86-64-Compilern, um 32-Bit-Code-Gen zu erhalten.

int sumint(int *arr) {
    int sum = 0;
    for (int i=0 ; i<2048 ; i++){
        sum += arr[i];
    }
    return sum;
}

innere Schleife mit gcc8.1 -O3 (ohne -march=haswell oder alles, um AVX/AVX2 zu aktivieren):

.L2:                                 # do {
    movdqu  xmm2, XMMWORD PTR [rdi]    # load 16 bytes
    add     rdi, 16
    paddd   xmm0, xmm2                 # packed add of 4 x 32-bit integers
    cmp     rax, rdi
    jne     .L2                      # } while(p != endp)

    # then horizontal add and extract a single 32-bit sum

Ohne -ffast-mathCompiler können FP-Operationen nicht neu anordnen, also die float Äquivalent nicht automatisch vektorisieren (siehe Godbolt-Link: Sie erhalten skalare addss). (OpenMP kann es auf Per-Loop-Basis aktivieren oder verwenden -ffast-math).

Aber einige FP-Sachen können sicher automatisch vektorisieren, ohne die Reihenfolge der Operationen zu ändern.

// clang won't contract this into an FMA without -ffast-math :/
// but gcc will (if you compile with -march=haswell)
void scale_array(float *arr) {
    for (int i=0 ; i<2048 ; i++){
        arr[i] = arr[i] * 2.1f + 1.234f;
    }
}

  # load constants: xmm2 = {2.1,  2.1,  2.1,  2.1}
  #                 xmm1 = (1.23, 1.23, 1.23, 1.23}
.L9:   # gcc8.1 -O3                       # do {
    movups  xmm0, XMMWORD PTR [rdi]         # load unaligned packed floats
    add     rdi, 16
    mulps   xmm0, xmm2                      # multiply Packed Single-precision
    addps   xmm0, xmm1                      # add Packed Single-precision
    movups  XMMWORD PTR [rdi-16], xmm0      # store back to the array
    cmp     rax, rdi
    jne     .L9                           # }while(p != endp)

Multiplikator = 2.0f führt zur Verwendung addps zu verdoppeln, wodurch der Durchsatz bei Haswell / Broadwell um den Faktor 2 reduziert wird! Denn vor SKL läuft FP add nur auf einem Ausführungsport, aber es gibt zwei FMA-Einheiten, die Multiplikationen ausführen können. SKL ließ den Addierer fallen und führt Add mit dem gleichen Durchsatz und der gleichen Latenz von 2 pro Takt wie mul und FMA aus. (http://agner.org/optimize/und sehen Sie sich weitere Leistungslinks im x86-Tag-Wiki an.)

Kompilieren mit -march=haswell lässt den Compiler einen einzigen FMA für die Skalierung + Addition verwenden. (Aber clang kontrahiert den Ausdruck nicht in eine FMA, es sei denn, Sie verwenden -ffast-math. IIRC gibt es eine Option, um die FP-Kontraktion ohne andere aggressive Operationen zu aktivieren.)

  • Compiler verwenden SSE auch an anderen Stellen, die nicht unter den traditionellen Mantel der (Schleifen-)Autovektorisierung fallen würden. Sogar bei -O1 und -Os beide clang und gcc Verwenden Sie beispielsweise SSE/AVX-Anweisungen für Strukturkopien.

    – BeeOnRope

    10. Juni 2018 um 20:31 Uhr

  • ” Spätere Befehlssätze (SSE4, AVX, AVX2, AVX512 und Nicht-SIMD-Erweiterungen wie BMI2, popcnt usw.) müssen manuell aktiviert werden um dem Compiler mitzuteilen, dass es in Ordnung ist, Code zu erstellen, der nicht auf älteren CPUs läuft.” — Was meinst du mit “müssen manuell aktiviert werden” ? Sie meinen, wir müssen die erforderlichen Compiler-Flags übergeben? Oder gibt es einige Einstellungen auf Systemebene, bei denen wir diese Funktionen aktivieren müssen? Ich vermute, es ist ersteres.

    – Nawaz

    26. Juli 2020 um 9:08 Uhr


  • @Nawaz: Ich meine Compiler-Flags, damit Code-Gen sie verwenden kann. Alle Mainstream-Kernel mit SSE-, AVX- und AVX512-Unterstützung setzen die CPU-Steuerregisterbits entsprechend, um sie standardmäßig zu aktivieren.

    – Peter Cordes

    26. Juli 2020 um 15:26 Uhr

  • @PeterCordes Vielen Dank für eine so aufschlussreiche Antwort.

    – picchiolu

    gestern

1437230cookie-checkVerwendet der Compiler SSE-Anweisungen für einen normalen C-Code?

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

Privacy policy