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?
(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:
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.
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.
.
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