Question

Je travaille sur une mémoire de style marque-version personnalisée allocateur pour le langage de programmation D qui fonctionne en allouant des régions locales de thread. Il semble que le fil goulot d'étranglement de stockage local est à l'origine d'un énorme (~ 50%) ralentissement de l'allocation de mémoire de ces régions par rapport à une seule par ailleurs identique version filetée du code, même après la conception de mon code pour avoir une seule TLS recherche par allocation / désaffectation. Ceci est basé sur l'allocation / libération de la mémoire d'un grand nombre de fois dans une boucle, et je suis en train de comprendre si c'est un artefact de ma méthode d'étalonnage. Je crois comprendre que le fil de stockage local devrait fondamentalement juste impliquer l'accès à quelque chose à travers une couche supplémentaire de indirection, similaire à accéder à une variable via un pointeur. Est-ce incorrect? Combien les frais généraux ne stockage local des threads ont généralement?

Note:. Bien que je mentionne D, je suis aussi intéressé par des réponses générales qui ne sont pas spécifiques à D, depuis la mise en œuvre du stockage local des threads de D'améliorera probablement si elle est plus lente que les meilleures implémentations

Était-ce utile?

La solution

La vitesse dépend de la mise en œuvre TLS.

Oui, vous avez raison que TLS peut être aussi rapide que d'une recherche de pointeur. Il peut même être plus rapide sur les systèmes avec une unité de gestion de la mémoire.

Pour la recherche de pointeur que vous avez besoin d'aide du planificateur bien. Le planificateur doit - sur un commutateur de tâche - mettre à jour le pointeur sur les données TLS.

Une autre façon rapide à mettre en œuvre TLS est par l'unité de gestion mémoire. Ici, le TLS est traité comme toutes les autres données à l'exception que les variables TLS sont alloués dans un segment spécial. Le planificateur - le commutateur de tâche - mapper le morceau correct de la mémoire dans l'espace d'adressage de la tâche.

Si le planificateur ne supporte aucune de ces méthodes, le compilateur / bibliothèque doit faire ce qui suit:

  • get ThreadId actuelle
  • Prenez sémaphores
  • Lookup le pointeur vers le bloc TLS par le ThreadId (peut utiliser une carte ou si)
  • Relâchez le sémaphores
  • Retour ce pointeur.

Il est évident que tout cela fait pour chaque accès aux données TLS prend un certain temps et peut nécessiter jusqu'à trois appels OS:. Obtenir le ThreadId, prendre et libérer les sémaphores

Le sémaphore est btw nécessaire pour se assurer qu'aucun thread lit de la liste des pointeurs TLS tandis qu'un autre thread est au milieu de la ponte d'un nouveau fil. (Et en tant que tel allouer un nouveau bloc TLS et de modifier la structure de données).

Malheureusement, il est pas rare de voir la mise en œuvre lente TLS dans la pratique.

Autres conseils

les habitants de la discussion en D sont vraiment rapides. Voici mes tests.

64 bits Ubuntu, i5 de base, dmd v2.052 Options du compilateur: DMD -O -release inline -m64

// this loop takes 0m0.630s
void main(){
    int a; // register allocated
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

// this loop takes 0m1.875s
int a; // thread local in D, not static
void main(){
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

Alors nous perdons seulement 1,2 secondes de l'un des noyaux de CPU par 1000 * 1000 * 1000 fil accès locaux. les habitants de la discussion sont accessibles en utilisant le registre% fs - donc il n'y a que quelques commandes de processeur impliquées:

avec objdump Désassemblage -d:

- this is local variable in %ecx register (loop counter in %eax):
   8:   31 c9                   xor    %ecx,%ecx
   a:   b8 00 ca 9a 3b          mov    $0x3b9aca00,%eax
   f:   83 c1 09                add    $0x9,%ecx
  12:   ff c8                   dec    %eax
  14:   85 c0                   test   %eax,%eax
  16:   75 f7                   jne    f <_Dmain+0xf>

- this is thread local, %fs register is used for indirection, %edx is loop counter:
   6:   ba 00 ca 9a 3b          mov    $0x3b9aca00,%edx
   b:   64 48 8b 04 25 00 00    mov    %fs:0x0,%rax
  12:   00 00 
  14:   48 8b 0d 00 00 00 00    mov    0x0(%rip),%rcx        # 1b <_Dmain+0x1b>
  1b:   83 04 08 09             addl   $0x9,(%rax,%rcx,1)
  1f:   ff ca                   dec    %edx
  21:   85 d2                   test   %edx,%edx
  23:   75 e6                   jne    b <_Dmain+0xb>

Peut-être que le compilateur pourrait être encore plus intelligent et le fil de cache local avant la boucle à un registre et le retourner au fil local à la fin (il est intéressant de comparer avec compilateur gdc), mais les choses encore maintenant sont très bons à mon humble avis.

Il faut être très prudent dans l'interprétation des résultats de référence. Par exemple, un fil récent dans les newsgroups D a conclu à partir d'une référence que la génération de code de DMD a été à l'origine d'un important ralentissement dans une boucle qui a fait l'arithmétique, mais en réalité, le temps passé a été dominé par la fonction d'aide à l'exécution qui a fait division. La génération de code du compilateur n'a rien à voir avec le ralentissement.

Pour voir quel genre de code est généré pour tls, compiler et obj2asm ce code:

__thread int x;
int foo() { return x; }

TLS est mis en œuvre de manière très différente sur Windows que sur Linux, et sera très différent à nouveau sur OSX. Mais, dans tous les cas, il sera beaucoup plus des instructions qu'un simple chargement d'un emplacement de mémoire statique. TLS va toujours être lent par rapport à l'accès simple. TLS dans un accès GLOBALS une boucle serrée va être trop lent,. Essayez la mise en cache la valeur TLS dans un lieu temporaire.

J'ai écrit il y a quelques années de code d'allocation de pool de threads, et la poignée en cache le TLS à la piscine, ce qui a bien fonctionné.

Si vous ne pouvez pas utiliser le support du compilateur TLS, vous pouvez gérer vous-même TLS. J'ai construit un modèle d'emballage pour C ++, il est donc facile de remplacer une mise en œuvre sous-jacente. Dans cet exemple, je l'ai mis en œuvre pour Win32. Remarque: Puisque vous ne pouvez pas obtenir un nombre illimité d'indices TLS par processus (au moins sous Win32), vous devez pointer vers tas de blocs assez grand pour contenir tous les fils de données spécifiques. De cette façon, vous avez un minimum d'indices TLS et des requêtes connexes. Dans le « meilleur des cas », vous auriez juste 1 pointeur TLS pointant vers un bloc de tas privé par fil.

En un mot:. Ne pas pointer vers des objets uniques, point au lieu de fil mémoire spécifiques, tas / récipients contenant des pointeurs d'objet pour obtenir de meilleures performances

Ne pas oublier de libérer de la mémoire si elle ne sert pas à nouveau. Je le fais en enveloppant un fil dans une classe (comme Java) et la poignée TLS par le constructeur et destructor. De plus, je stocke des données fréquemment utilisées comme poignées de fil et les ID en tant que membres de la classe.

utilisation:

  

pour le type *:   tl_ptr

     

pour le type const *:   tl_ptr

     

pour le type * const:   const tl_ptr

     

type const * const:   const tl_ptr

template<typename T>
class tl_ptr {
protected:
    DWORD index;
public:
    tl_ptr(void) : index(TlsAlloc()){
        assert(index != TLS_OUT_OF_INDEXES);
        set(NULL);
    }
    void set(T* ptr){
        TlsSetValue(index,(LPVOID) ptr);
    }
    T* get(void)const {
        return (T*) TlsGetValue(index);
    }
    tl_ptr& operator=(T* ptr){
        set(ptr);
        return *this;
    }
    tl_ptr& operator=(const tl_ptr& other){
        set(other.get());
        return *this;
    }
    T& operator*(void)const{
        return *get();
    }
    T* operator->(void)const{
        return get();
    }
    ~tl_ptr(){
        TlsFree(index);
    }
};

Je l'ai conçu multitâches pour les systèmes embarqués et sur le plan conceptuel l'exigence clé pour le stockage local des threads est d'avoir la méthode de changement de contexte save / restore un pointeur vers le stockage thread local ainsi que les registres CPU et tout ce qu'il économise / restauration. Pour les systèmes embarqués qui sera toujours en cours d'exécution le même ensemble de code une fois qu'ils ont commencé en place, il est plus facile de simplement sauvegarder / restaurer un pointeur qui pointe vers un bloc de format fixe pour chaque fil. Agréable, propre, facile et efficace.

Une telle approche fonctionne bien si l'on ne me dérange pas d'avoir un espace pour chaque variable de thread local alloués dans chaque thread - même ceux qui ne l'utilisent jamais réellement - et si tout ce qui va être dans le stockage local des threads bloc peut être définie comme une seule structure. Dans ce scénario, les accès aux variables locales de thread peut être presque aussi rapide que l'accès à d'autres variables, la seule différence étant un déréférencement de pointeur supplémentaire. Malheureusement, de nombreuses applications PC exigent quelque chose de plus compliqué.

Sur certains cadres pour le PC, un fil n'aura espace alloué pour les variables de fil statique si un module qui utilise ces variables a été exécuté sur ce thread. Même si cela peut parfois être avantageux, cela signifie que les différents threads ont souvent leur stockage local aménagé différemment. Par conséquent, il peut être nécessaire pour les fils d'avoir une sorte d'index de recherche de l'endroit où leurs variables se trouvent, et à tous les accès directs à ces variables par cet indice.

Je pense que si le cadre alloue une petite quantité de stockage au format fixe, il peut être utile de garder un cache des dernières 1-3 des variables locales de thread accessibles, car dans de nombreux scénarios, même un cache unique élément pourrait offrir un taux de réussite assez élevé.

Nous avons vu des problèmes de performance similaires de TLS (sous Windows). Nous comptons sur elle pour certaines opérations critiques à l'intérieur « noyau de notre produit. Après un certain effort, j'ai décidé d'essayer et d'améliorer à ce sujet.

Je suis heureux de dire que nous avons maintenant une petite API qui offre> 50% de réduction du temps CPU pour une opération équivalente lorsque le fil de Callin ne « sait » pas son fil-id et> 65% de réduction si vous appelez fil a déjà obtenu son fil-id (peut-être pour une autre étape de traitement antérieure).

La nouvelle fonction (get_thread_private_ptr ()) renvoie toujours un pointeur vers une struct que nous utilisons en interne pour contenir toutes sortes, nous avons donc besoin d'un seul par fil.

Dans l'ensemble je pense que le support TLS Win32 est mal conçu vraiment.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top