Was ist der Leistungsnachteil von C ++ 11 thread_local-Variablen in GCC 4.8?


71

Aus dem GCC 4.8-Änderungsprotokollentwurf :

G ++ implementiert jetzt das Schlüsselwort C ++ 11 thread_local ; Dies unterscheidet sich vom GNU- __threadSchlüsselwort hauptsächlich dadurch, dass es eine dynamische Initialisierungs- und Zerstörungssemantik ermöglicht. Leider erfordert diese Unterstützung eine Laufzeitstrafe für Verweise auf nicht funktionslokale thread_localVariablen, auch wenn sie keine dynamische Initialisierung benötigen. Daher möchten Benutzer möglicherweise weiterhin __threadTLS-Variablen mit statischer Initialisierungssemantik verwenden.

Was genau ist die Art und der Ursprung dieser Laufzeitstrafe?

Um nicht funktionslokale thread_localVariablen zu unterstützen, muss natürlich vor dem Eintritt in jede Thread-Hauptphase eine Thread-Initialisierungsphase stattfinden (genau wie es eine statische Initialisierungsphase für globale Variablen gibt) ?

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


5
Ich denke wirklich, dass die GCC-Mailingliste ein besserer Ort ist, um zu fragen (und höchstwahrscheinlich eine Antwort zu erhalten, obwohl Jonathan Wakely und andere GCC / libstdc ++ - Entwickler hier lauern und möglicherweise mehr wissen). Trotzdem interessante Frage.
Xeo

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

Antworten:


49

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

Die dynamische thread_localInitialisierung wird in Commit 462819c hinzugefügt . 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_localVariablen zu einem Funktionsaufruf wird. Lassen Sie uns 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;

Beim Kompilieren * sehen wir, dass jede Verwendung der tlsReferenz zu einem Funktionsaufruf wird _ZTW3tls, der die Variable einmal träge initialisiert:

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

Vergleichen Sie es mit der __threadVersion, die diesen zusätzlichen Wrapper nicht hat:

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 jedoch nicht in jedem Anwendungsfall benötigt thread_local. Dies kann aus entnommen werden 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 für __threadVariablen nicht zulässig ist) oder
    3. Die Typvariable wird durch einen nicht konstanten Ausdruck initialisiert (was auch für __threadVariablen nicht zulässig ist).

In allen anderen Anwendungsfällen verhält es sich genauso wie __thread. Das heißt, es sei denn , Sie einige haben extern __threadVariablen, könnten Sie alle ersetzen __threaddurch , thread_localohne Leistungsverlust.


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


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

15
@ MSalters, Patches sind willkommen, wenn Sie es verbessern können! :) Der Thread, der unter gcc.gnu.org/ml/gcc/2012-10/msg00024.html beginnt, hat einige relevante Diskussionen
Jonathan Wakely

10

C ++ 11 thread_local hat den gleichen Laufzeiteffekt wie der __thread-Bezeichner ( __threadist nicht Teil des C-Standards; thread_localist Teil des C ++ - Standards)

Dies hängt davon ab, wo die TLS-Variable (mit dem Bezeichner deklariert __thread) deklariert ist.

  • Wenn die TLS-Variable in einer ausführbaren Datei deklariert ist, ist der Zugriff schnell
  • Wenn die TLS-Variable im gemeinsam genutzten Bibliothekscode deklariert ist (kompiliert mit der -fPICCompiler-Option) und die -ftls-model=initial-execCompiler-Option angegeben ist, ist der Zugriff schnell. Es gilt jedoch die folgende Einschränkung: Die gemeinsam genutzte Bibliothek kann nicht über dlopen / dlsym geladen werden (dynamisches Laden). Die einzige Möglichkeit, die Bibliothek zu verwenden, besteht darin, sie während der Kompilierung zu verknüpfen (Linker-Option -l<libraryname>).
  • Wenn die TLS-Variable in einer gemeinsam genutzten Bibliothek deklariert ist ( -fPICCompiler-Optionssatz), 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 in der Art und Weise, wie die gemeinsam genutzte Bibliothek verwendet wird, nicht eingeschränkt sind.

Quellen: ELF-Handling für threadlokalen Speicher von Ulrich Drepper https://www.akkadia.org/drepper/tls.pdf In diesem Text wird auch der Code aufgeführt, der für die unterstützten Zielplattformen generiert wird.


9

Wenn die Variable in der aktuellen TU definiert ist, kümmert sich der Inliner um den Overhead. Ich gehe davon aus, dass dies für die meisten Verwendungen von thread_local gilt.

Wenn der Programmierer bei externen Variablen 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 zuvor ausgeführt wird) Bei Verwendung in einer anderen TU) können sie diesen Overhead mit der Option -fno-extern-tls-init vermeiden.


1
Fast immer benutze ich thread_localein Muster wie T& f() { thread_local t; return t; }. Ich verwende gcc 4.7 und verwende derzeit eine "Problemumgehung", um thread_local zu implementieren, die ich hier geschrieben habe: stackoverflow.com/q/12049684/1131467 . Wie ist der Overhead der 4.8-Implementierung im Vergleich zu meiner 4.7-Workaround-Implementierung für den Fall der fFunktion?
Andrew Tomazos

Hier ist ein direkter Link zu 4.7 Workaround: stackoverflow.com/a/12053862/1131467
Andrew Tomazos

Der Eintrag in den Versionshinweisen befasst sich mit 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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.