Was ist die Leistungseinbuße von C++11 thread_local-Variablen in GCC 4.8?

Lesezeit: 7 Minuten

Was ist die Leistungseinbuse von C11 thread local Variablen in GCC 48
Andrew Tomazos

Von dem GCC 4.8-Entwurf des Änderungsprotokolls:

G++ implementiert jetzt die C++11 thread_local Stichwort; dies unterscheidet sich von GNU __thread Schlüsselwort hauptsächlich dadurch, dass es eine dynamische Initialisierungs- und Zerstörungssemantik ermöglicht. Leider erfordert diese Unterstützung eine Laufzeiteinbuße für Verweise auf nicht-funktionslokale
thread_local Variablen, auch wenn sie keine dynamische Initialisierung benötigen, sodass Benutzer möglicherweise weiterhin verwenden möchten __thread für TLS-Variablen mit statischer Initialisierungssemantik.

Was genau ist die Art und der Ursprung dieser Laufzeitstrafe?

Offensichtlich um nicht-funktionslokale zu unterstützen thread_local Variablen muss eine Thread-Initialisierungsphase vor dem Eintritt in jeden Thread main sein (genauso wie es eine statische Initialisierungsphase für globale Variablen gibt), aber beziehen sie sich auf eine darüber hinausgehende Laufzeiteinbuße?

Was ist grob gesagt die Architektur der neuen Implementierung von thread_local in gcc?

  • Ich denke wirklich, die GCC-Mailingliste wäre ein besserer Ort, um zu fragen (und höchstwahrscheinlich eine Antwort zu erhalten, obwohl Jonathan Wakely und andere GCC/libstdc++-Entwickler hier lauern und vielleicht mehr wissen). Trotzdem interessante Frage.

    – Xeo

    28. Okt ’12 um 5:06


  • Es gibt einige relevante Diskussionen im Thread, beginnend mit gcc.gnu.org/ml/gcc/2012-10/msg00024.html

    – Jonathan Wakely

    7. Januar ’13 um 23:06

(Haftungsausschluss: Ich weiß nicht viel über die Interna von GCC, daher ist dies auch eine begründete Vermutung.)

Die Dynamik thread_local Initialisierung wird in Commit hinzugefügt 462819c. Eine der Änderungen ist:

* semantics.c (finish_id_expression): Replace use of thread_local
variable with a call to its wrapper.

Die Laufzeitstrafe besteht also darin, dass jede Referenz der thread_local Variable wird zu einem Funktionsaufruf. Lassen Sie uns das mit einem einfachen Testfall überprüfen:

// 3.cpp
extern thread_local int tls;    
int main() {
    tls += 37;   // line 6
    tls &= 11;   // line 7
    tls ^= 3;    // line 8
    return 0;
}

// 4.cpp

thread_local int tls = 42;

Wenn kompiliert*, sehen wir das jeden Verwendung der tls Referenz wird zu einem Funktionsaufruf von _ZTW3tls, die die Variable einmal träge initialisieren:

00000000004005b0 <main>:
main():
  4005b0:   55                          push   rbp
  4005b1:   48 89 e5                    mov    rbp,rsp
  4005b4:   e8 26 00 00 00              call   4005df <_ZTW3tls>    // line 6
  4005b9:   8b 10                       mov    edx,DWORD PTR [rax]
  4005bb:   83 c2 25                    add    edx,0x25
  4005be:   89 10                       mov    DWORD PTR [rax],edx
  4005c0:   e8 1a 00 00 00              call   4005df <_ZTW3tls>    // line 7
  4005c5:   8b 10                       mov    edx,DWORD PTR [rax]
  4005c7:   83 e2 0b                    and    edx,0xb
  4005ca:   89 10                       mov    DWORD PTR [rax],edx
  4005cc:   e8 0e 00 00 00              call   4005df <_ZTW3tls>    // line 8
  4005d1:   8b 10                       mov    edx,DWORD PTR [rax]
  4005d3:   83 f2 03                    xor    edx,0x3
  4005d6:   89 10                       mov    DWORD PTR [rax],edx
  4005d8:   b8 00 00 00 00              mov    eax,0x0              // line 9
  4005dd:   5d                          pop    rbp
  4005de:   c3                          ret

00000000004005df <_ZTW3tls>:
_ZTW3tls():
  4005df:   55                          push   rbp
  4005e0:   48 89 e5                    mov    rbp,rsp
  4005e3:   b8 00 00 00 00              mov    eax,0x0
  4005e8:   48 85 c0                    test   rax,rax
  4005eb:   74 05                       je     4005f2 <_ZTW3tls+0x13>
  4005ed:   e8 0e fa bf ff              call   0 <tls> // initialize the TLS
  4005f2:   64 48 8b 14 25 00 00 00 00  mov    rdx,QWORD PTR fs:0x0
  4005fb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  400602:   48 01 d0                    add    rax,rdx
  400605:   5d                          pop    rbp
  400606:   c3                          ret

Vergleiche es mit dem __thread Version, die diesen zusätzlichen Wrapper nicht haben wird:

00000000004005b0 <main>:
main():
  4005b0:   55                          push   rbp
  4005b1:   48 89 e5                    mov    rbp,rsp
  4005b4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 6
  4005bb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005be:   8d 50 25                    lea    edx,[rax+0x25]
  4005c1:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005c8:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005cb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 7
  4005d2:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005d5:   89 c2                       mov    edx,eax
  4005d7:   83 e2 0b                    and    edx,0xb
  4005da:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005e1:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005e4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 8
  4005eb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005ee:   89 c2                       mov    edx,eax
  4005f0:   83 f2 03                    xor    edx,0x3
  4005f3:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005fa:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005fd:   b8 00 00 00 00              mov    eax,0x0                // line 9
  400602:   5d                          pop    rbp
  400603:   c3                          ret

Dieser Wrapper wird nicht in jedem Anwendungsfall von . benötigt thread_local obwohl. Dies kann verraten werden aus decl2.c. Der Wrapper wird nur generiert, wenn:

  • es ist nicht funktionslokal und

    1. es ist extern (das oben gezeigte Beispiel), oder
    2. Der Typ hat einen nicht-trivialen Destruktor (der nicht erlaubt ist für __thread Variablen) oder
    3. Die Typvariable wird durch einen non-constant-expression initialisiert (was auch nicht erlaubt ist für __thread Variablen).

In allen anderen Anwendungsfällen verhält es sich genauso wie __thread. Das heißt, es sei denn, Sie haben welche extern __thread Variablen, du könntest alle ersetzen __thread durch thread_local ohne Leistungsverlust.


*: Ich habe mit -O0 kompiliert, da der Inliner die Funktionsgrenze weniger sichtbar macht. Selbst wenn wir auf -O3 auftauchen, bleiben diese Initialisierungsprüfungen bestehen.

  • Das ist bemerkenswert dumm. Sicher, das bedeutet, dass Sie keine Anrufflussanalyse durchführen müssen, um festzustellen, ob ein vorheriger Zugriff auf vorliegt tls, aber selbst die naivste Analyse hätte festgestellt, dass der Zugriff auf Zeile 7 absolut nicht der erste Zugriff sein kann.

    – MSalters

    29. Okt ’12 um 16:17

  • @MSalters, Patches willkommen, wenn Sie sie verbessern können! 🙂 Der Thread, der um beginnt gcc.gnu.org/ml/gcc/2012-10/msg00024.html hat eine relevante Diskussion

    – Jonathan Wakely

    7. Januar ’13 um 23:06

Was ist die Leistungseinbuse von C11 thread local Variablen in GCC 48
MichaelMoser

C++11 thread_local hat denselben Laufzeiteffekt wie der __thread-Spezifizierer (__thread ist nicht Bestandteil des C-Standards; thread_local ist Teil des C++-Standards)

es hängt davon ab, wo die TLS-Variable (deklariert mit __thread Spezifizierer) wird deklariert.

  • Wenn die TLS-Variable in einer ausführbaren Datei deklariert ist, ist der Zugriff schnell
  • wenn die TLS-Variable im Code der gemeinsam genutzten Bibliothek deklariert ist (kompiliert mit -fPIC Compileroption) und -ftls-model=initial-exec Compileroption wird angegeben, dann ist der Zugriff schnell; es gilt jedoch folgende Einschränkung: die Shared Library kann nicht über dlopen/dlsym geladen werden (dynamisches Laden), die einzige Möglichkeit die Bibliothek zu verwenden besteht darin, während der Kompilierung mit ihr zu verknüpfen (Linker-Option -l<libraryname> )
  • wenn die TLS-Variable in einer gemeinsam genutzten Bibliothek deklariert ist (-fPIC Compiler-Optionssatz) dann ist der Zugriff sehr langsam, da das allgemeine dynamische TLS-Modell angenommen wird – hier führt jeder Zugriff auf eine TLS-Variable zu einem Aufruf von _tls_get_addr() ; Dies ist der Standardfall, da Sie bei der Verwendung der gemeinsam genutzten Bibliothek nicht eingeschränkt sind.

Quellen: ELF-Handling für Thread-lokale Speicherung von Ulrich Drepper
https://www.akkadia.org/drepper/tls.pdf
Dieser Text listet auch den Code auf, der für die unterstützten Zielplattformen generiert wird.

Wenn die Variable in der aktuellen TU definiert ist, übernimmt der Inliner den Overhead. Ich gehe davon aus, dass dies für die meisten Verwendungen von thread_local zutrifft.

Bei externen Variablen, wenn der Programmierer sicher sein kann, dass keine Verwendung der Variablen in einer nicht-definierenden TU eine dynamische Initialisierung auslösen muss (entweder weil die Variable statisch initialisiert ist oder eine Verwendung der Variablen in der definierenden TU vorher ausgeführt wird) jegliche Verwendung in einer anderen TU), können sie diesen Overhead mit der Option -fno-extern-tls-init vermeiden.

  • Fast ausnahmslos meine Verwendung von thread_local ist über ein Muster wie T& f() { thread_local t; return t; }. Ich verwende gcc 4.7, daher verwende ich derzeit einen “Workaround”, um thread_local zu implementieren, den ich hier geschrieben habe: stackoverflow.com/q/12049684/1131467. Wie verhält sich der Overhead der 4.8-Implementierung im Vergleich zu meiner 4.7-Workaround-Implementierung für den Fall der f Funktion?

    – Andrew Tomazos

    16. Feb. ’13 um 17:32


  • Hier ist ein direkter Link zur Problemumgehung von 4.7: stackoverflow.com/a/12053862/1131467

    – Andrew Tomazos

    16. Feb. ’13 um 17:38 Uhr

  • Der Eintrag in den Versionshinweisen spricht von nicht-funktionslokalen Variablen; für eine lokale Variable wie t in Ihrem Beispiel sollte die 4.8-Implementierung Ihrer Problemumgehung ähnlich oder etwas effizienter sein.

    – Jason Merrill

    17. April ’13 um 21:31

.

295890cookie-checkWas ist die Leistungseinbuße von C++11 thread_local-Variablen in GCC 4.8?

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

Privacy policy