65 votes

Quel est le coût de performance des variables thread_local de C++11 dans GCC 4.8?

Depuis le brouillon du journal des modifications de GCC 4.8:

G++ implémente désormais le mot-clé C++11 thread_local; cela diffère du mot-clé GNU __thread principalement en ce qu'il permet des sémantiques d'initialisation et de destruction dynamiques. Malheureusement, cette prise en charge entraîne une pénalité d'exécution pour les références aux variables thread_local non locales à la fonction même si elles n'ont pas besoin d'une initialisation dynamique, les utilisateurs peuvent donc continuer à utiliser __thread pour les variables TLS avec des sémantiques d'initialisation statique.

Quelle est précisément la nature et l'origine de cette pénalité d'exécution?

Évidemment, pour prendre en charge les variables thread_local non locales à la fonction, il doit y avoir une phase d'initialisation des threads avant l'entrée dans chaque thread principal (tout comme il y a une phase d'initialisation statique pour les variables globales), mais font-ils référence à une pénalité d'exécution supplémentaire au-delà de cela?

En gros, quelle est l'architecture de la nouvelle implémentation de thread_local de gcc?

47voto

KennyTM Points 232647

(Avertissement : Je ne connais pas très bien les détails internes de GCC, donc ceci est aussi une supposition éduquée.)

L'initialisation dynamique thread_local est ajoutée dans le commit 462819c. Un des changements est le suivant :

* semantics.c (finish_id_expression) : Remplacer l'utilisation de la variable thread_local
par un appel à son wrapper.

Donc, la pénalité d'exécution est que chaque référence à la variable thread_local deviendra un appel de fonction. Vérifions avec un exemple simple :

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

// 4.cpp

thread_local int tls = 42;

Lors de la compilation*, on voit que chaque utilisation de la référence tls devient un appel de fonction à _ZTW3tls, qui initialise de manière paresseuse la variable une fois :

00000000004005b0 

Comparez-le avec la version __thread, qui n'aura pas ce wrapper supplémentaire :

00000000004005b0 

Ce wrapper n'est pas nécessaire pour tous les cas d'utilisation de thread_local`decl2.c`. Le wrapper est généré seulement lorsque :

``

  • Il n'est pas local à la fonction, et,

    1. Il est extern (exemple montré ci-dessus), ou
    2. Le type a un destructeur non trivial (ce qui n'est pas autorisé pour les variables __thread), ou
    3. La variable de type est initialisée par une expression non constante (ce qui n'est pas autorisé non plus pour les variables __thread).

Dans tous les autres cas d'utilisation, il se comporte de la même manière que __thread. Cela signifie que sauf si vous avez des variables extern __thread, vous pourriez remplacer tous les __thread par thread_local sans aucune perte de performance.


  • : J'ai compilé avec -O0 car l'inliner rendra la limite de fonction moins visible. Même si nous augmentons à -O3, ces vérifications d'initialisation restent toujours.

``

15voto

MichaelMoser Points 169

Le thread_local de C++11 a le même effet à l'exécution que le spécificateur __thread (__thread ne fait pas partie de la norme C; thread_local fait partie de la norme C++)

cela dépend de l'endroit où la variable TLS (déclarée avec le spécificateur __thread) est déclarée.

  • si la variable TLS est déclarée dans un exécutable, alors l'accès est rapide
  • si la variable TLS est déclarée dans du code de bibliothèque partagée (compilé avec l'option de compilateur -fPIC) et que l'option de compilateur -ftls-model=initial-exec est spécifiée, alors l'accès est rapide; cependant, la limitation suivante s'applique : la bibliothèque partagée ne peut pas être chargée via dlopen/dlsym (chargement dynamique), la seule façon d'utiliser la bibliothèque est de la lier lors de la compilation (option de liaison -l )
  • si la variable TLS est déclarée dans une bibliothèque partagée (option de compilateur -fPIC définie) alors l'accès est très lent, car le modèle général de TLS dynamique est supposé - ici, chaque accès à une variable TLS entraîne un appel à _tls_get_addr() ; c'est le cas par défaut car vous n'êtes pas limité dans la façon dont la bibliothèque partagée est utilisée.

Sources : Traitement ELF pour le stockage local par thread par Ulrich Drepper https://www.akkadia.org/drepper/tls.pdf ce texte répertorie également le code généré pour les plates-formes cibles prises en charge.

9voto

Jason Merrill Points 116

Si la variable est définie dans l'unité de traduction actuelle, l'inlinage se chargera des frais généraux. Je m'attends à ce que cela soit vrai pour la plupart des utilisations de thread_local.

Pour les variables externes, si le programmeur peut être sûr qu'aucune utilisation de la variable dans une unité de traduction non définissante ne doit déclencher d'initialisation dynamique (soit parce que la variable est initialisée de manière statique, soit qu'une utilisation de la variable dans l'unité de définition sera exécutée avant toute utilisation dans une autre unité de traduction), ils peuvent éviter ces frais avec l'option -fno-extern-tls-init.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X