web-dev-qa-db-fra.com

Comment estimer la surcharge de commutation de contexte de thread?

J'essaie d'améliorer les performances de l'application filetée avec des délais en temps réel. Il fonctionne sur Windows Mobile et écrit en C/C++. Je soupçonne qu'une fréquence élevée de commutation de threads peut entraîner des frais généraux tangibles, mais je ne peux ni le prouver ni le réfuter. Comme tout le monde le sait, le manque de preuves n'est pas une preuve d'opposé :).

Ma question est donc double:

  • S'il existe, où puis-je trouver des mesures réelles du coût de commutation du contexte de thread?

  • Sans passer du temps à écrire une application de test, quels sont les moyens d'estimer la surcharge de commutation de threads dans l'application existante?

  • Quelqu'un connaît-il un moyen de connaître le nombre de changements de contexte (marche/arrêt) pour un thread donné?

57
Ignas Limanauskas

Alors que vous avez dit que vous ne vouliez pas écrire une application de test, je l'ai fait pour un test précédent sur une plate-forme ARM9 Linux pour découvrir ce qu'est la surcharge. Ce n'était que deux threads qui stimuleraient :: thread :: yield () (ou, vous savez) et incrémenteraient certaines variables, et après une minute environ (sans autres processus en cours, au moins aucun qui fait quelque chose), l'application a imprimé combien de changements de contexte il pourrait faire par seconde. Bien sûr, ce n'est pas vraiment exact, mais le fait est que les deux threads se cédaient le processeur, et c'était si rapide que cela n'avait plus de sens de penser à la surcharge. Alors, allez-y et écrivez simplement un test simple au lieu de trop penser à un problème qui peut être inexistant.

En dehors de cela, vous pouvez essayer comme 1800 l'a suggéré avec des compteurs de performance.

Oh, et je me souviens d'une application fonctionnant sous Windows CE 4.X, où nous avons également quatre threads avec une commutation intensive à certains moments, et nous n'avons jamais rencontré de problèmes de performances. Nous avons également essayé d'implémenter le noyau de threading sans threads, et nous n'avons vu aucune amélioration des performances (l'interface graphique a simplement répondu beaucoup plus lentement, mais tout le reste était le même). Vous pouvez peut-être essayer la même chose, soit en réduisant le nombre de changements de contexte, soit en supprimant complètement les threads (juste pour les tests).

13
OregonGhost

Je doute que vous puissiez trouver cette surcharge quelque part sur le Web pour n'importe quelle plate-forme existante. Il existe tout simplement trop de plateformes différentes. Les frais généraux dépendent de deux facteurs:

  • Le CPU, car les opérations nécessaires peuvent être plus faciles ou plus difficiles sur différents types de CPU
  • Le noyau du système, car différents noyaux devront effectuer différentes opérations sur chaque commutateur

D'autres facteurs incluent la façon dont le changement a lieu. Un changement peut avoir lieu lorsque

  1. le thread a utilisé tout son temps quantique. Lorsqu'un thread est démarré, il peut s'exécuter pendant un certain temps avant de devoir retourner le contrôle au noyau qui décidera qui sera le prochain.

  2. le fil a été préempté. Cela se produit lorsqu'un autre thread a besoin de temps CPU et a une priorité plus élevée. Par exemple. le thread qui gère la saisie souris/clavier peut être un tel thread. Quel que soit le thread possède le CPU en ce moment, lorsque l'utilisateur tape quelque chose ou clique sur quelque chose, il ne veut pas attendre que le quantum de temps des threads actuels soit complètement utilisé, il veut voir le le système réagit immédiatement. Ainsi, certains systèmes arrêteront immédiatement le thread actuel et rendront le contrôle à un autre thread avec une priorité plus élevée.

  3. le thread n'a plus besoin de temps CPU, car il bloque une opération ou est simplement appelé sleep () (ou similaire) pour arrêter de fonctionner.

Ces 3 scénarios peuvent avoir des temps de commutation de threads différents en théorie. Par exemple. Je m'attendrais à ce que le dernier soit le plus lent, car un appel à sleep () signifie que le processeur est rendu au noyau et que le noyau doit configurer un appel de réveil qui s'assurera que le thread est réveillé après environ la durée pendant laquelle il a demandé à s'endormir, il doit ensuite retirer le thread du processus de planification et, une fois le thread réveillé, il doit à nouveau l'ajouter au processus de planification. Toutes ces étapes prendront un certain temps. Ainsi, l'appel de sommeil réel peut être plus long que le temps nécessaire pour passer à un autre thread.

Je pense que si vous voulez savoir avec certitude, vous devez comparer. Le problème est que vous devrez généralement mettre les threads en veille ou les synchroniser à l'aide de mutex. Les mutex endormis ou verrouillables/déverrouillables ont eux-mêmes des frais généraux. Cela signifie que votre référence inclura également ces frais généraux. Sans un puissant profileur, il est difficile de dire plus tard combien de temps CPU a été utilisé pour le commutateur réel et combien pour l'appel sleep/mutex. D'un autre côté, dans un scénario réel, vos threads seront également mis en veille ou synchronisés via des verrous. Une référence qui mesure purement le temps de changement de contexte est une référence synthétique car elle ne modélise aucun scénario réel. Les repères sont beaucoup plus "réalistes" s'ils reposent sur des scénarios réels. À quoi sert un benchmark GPU qui me dit que mon GPU peut en théorie gérer 2 milliards de polygones par seconde, si ce résultat ne peut jamais être atteint dans une application 3D réelle? Ne serait-il pas beaucoup plus intéressant de savoir combien de polygones une application 3D réelle peut permettre au GPU de gérer une seconde?

Malheureusement, je ne connais rien à la programmation Windows. Je pourrais écrire une application pour Windows en Java ou peut-être en C #, mais C/C++ sur Windows me fait pleurer. Je ne peux que vous proposer du code source pour POSIX.

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>

uint32_t COUNTER;
pthread_mutex_t LOCK;
pthread_mutex_t START;
pthread_cond_t CONDITION;

void * threads (
    void * unused
) {
    // Wait till we may fire away
    pthread_mutex_lock(&START);
    pthread_mutex_unlock(&START);

    pthread_mutex_lock(&LOCK);
    // If I'm not the first thread, the other thread is already waiting on
    // the condition, thus Ihave to wake it up first, otherwise we'll deadlock
    if (COUNTER > 0) {
        pthread_cond_signal(&CONDITION);
    }
    for (;;) {
        COUNTER++;
        pthread_cond_wait(&CONDITION, &LOCK);
        // Always wake up the other thread before processing. The other
        // thread will not be able to do anything as long as I don't go
        // back to sleep first.
        pthread_cond_signal(&CONDITION);
    }
    pthread_mutex_unlock(&LOCK); //To unlock
}

int64_t timeInMS ()
{
    struct timeval t;

    gettimeofday(&t, NULL);
    return (
        (int64_t)t.tv_sec * 1000 +
        (int64_t)t.tv_usec / 1000
    );
}


int main (
    int argc,
    char ** argv
) {
    int64_t start;
    pthread_t t1;
    pthread_t t2;
    int64_t myTime;

    pthread_mutex_init(&LOCK, NULL);
    pthread_mutex_init(&START, NULL);   
    pthread_cond_init(&CONDITION, NULL);

    pthread_mutex_lock(&START);
    COUNTER = 0;
    pthread_create(&t1, NULL, threads, NULL);
    pthread_create(&t2, NULL, threads, NULL);
    pthread_detach(t1);
    pthread_detach(t2);
    // Get start time and fire away
    myTime = timeInMS();
    pthread_mutex_unlock(&START);
    // Wait for about a second
    sleep(1);
    // Stop both threads
    pthread_mutex_lock(&LOCK);
    // Find out how much time has really passed. sleep won't guarantee me that
    // I sleep exactly one second, I might sleep longer since even after being
    // woken up, it can take some time before I gain back CPU time. Further
    // some more time might have passed before I obtained the lock!
    myTime = timeInMS() - myTime;
    // Correct the number of thread switches accordingly
    COUNTER = (uint32_t)(((uint64_t)COUNTER * 1000) / myTime);
    printf("Number of thread switches in about one second was %u\n", COUNTER);
    return 0;
}

Production

Number of thread switches in about one second was 108406

Plus de 100'000 n'est pas trop mal et cela même si nous avons des blocages et des attentes conditionnelles. Je suppose que sans tout cela au moins deux fois plus de commutateurs de threads étaient possibles une seconde.

26
Mecki

Vous ne pouvez pas l'estimer. Vous devez le mesurer. Et cela va varier en fonction du processeur de l'appareil.

Il existe deux façons assez simples de mesurer un changement de contexte. L'un implique du code, l'autre non.

Tout d'abord, la voie du code (pseudocode):

DWORD tick;

main()
{
  HANDLE hThread = CreateThread(..., ThreadProc, CREATE_SUSPENDED, ...);
  tick = QueryPerformanceCounter();
  CeSetThreadPriority(hThread, 10); // real high
  ResumeThread(hThread);
  Sleep(10);
}

ThreadProc()
{
  tick = QueryPerformanceCounter() - tick;
  RETAILMSG(TRUE, (_T("ET: %i\r\n"), tick));
}

Évidemment, le faire en boucle et faire la moyenne sera mieux. Gardez à l'esprit que cela ne mesure pas seulement le changement de contexte. Vous mesurez également l'appel à ResumeThread et il n'y a aucune garantie que le planificateur passera immédiatement à votre autre thread (bien que la priorité de 10 devrait aider à augmenter les chances qu'il le fasse).

Vous pouvez obtenir une mesure plus précise avec CeLog en vous connectant aux événements du planificateur, mais c'est loin d'être simple à faire et pas très bien documenté. Si vous voulez vraiment suivre cette voie, Sue Loh a plusieurs blogs sur lesquels un moteur de recherche peut trouver.

L'itinéraire non-code serait d'utiliser Remote Kernel Tracker. Installez eVC 4.0 ou la version eval de Platform Builder pour l'obtenir. Il donnera un affichage graphique de tout ce que le noyau fait et vous pouvez directement mesurer un changement de contexte de thread avec les capacités de curseur fournies. Encore une fois, je suis certain que Sue a également une entrée de blog sur l'utilisation de Kernel Tracker.

Cela dit, vous constaterez que les commutateurs de contexte de thread intra-processus CE sont vraiment très rapides. Ce sont les commutateurs de processus qui sont coûteux, car cela nécessite d'échanger le processus actif dans RAM puis de faire la migration.

14
ctacke

Mon 50 lignes de C++ affiche pour Linux (QuadCore Q6600) le temps de changement de contexte ~ 0.9us (0.75us pour 2 threads, 0.95 pour 50 threads). Dans cette référence, les threads appellent le rendement immédiatement lorsqu'ils obtiennent un quantum de temps.

7
bobah

Le changement de contexte est coûteux, en règle générale, il coûte 30µs de temps processeur http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context. html

6
Soroush

Je n'ai essayé d'estimer cela qu'une seule fois et c'était sur un 486! Le résultat a été que le changement de contexte du processeur prenait environ 70 instructions à compléter (notez que cela se produisait pour de nombreux appels d'api du système d'exploitation ainsi que pour le changement de thread). Nous avons calculé qu'il fallait environ 30us par commutateur de thread (y compris la surcharge du système d'exploitation) sur un DX3. Les quelques milliers de changements de contexte que nous faisions par seconde absorbaient entre 5 et 10% du temps du processeur.

Comment cela se traduirait en un processeur moderne multi-cœurs et multi-ghz, je ne sais pas, mais je suppose que sauf si vous alliez complètement au-dessus avec un changement de thread, c'est une surcharge négligeable.

Notez que la création/suppression de threads est un gestionnaire de processeur/système d'exploitation plus coûteux que l'activation/désactivation de threads. Une bonne politique pour les applications fortement threadées consiste à utiliser des pools de threads et à activer/désactiver selon les besoins.

5
Tim Ring

Le problème avec les changements de contexte est qu'ils ont une heure fixe. Le GPU a implémenté un changement de contexte à 1 cycle entre les threads. Par exemple, les éléments suivants ne peuvent pas être enfilés sur les processeurs:

double * a; 
...
for (i = 0; i < 1000; i ++)
{
    a[i] = a[i] + a[i]
}

car son temps d'exécution est bien inférieur au coût de changement de contexte. Sur Core i7, ce code prend environ 1 micro seconde (dépend du compilateur). L'heure de changement de contexte est donc importante car elle définit comment les petits travaux peuvent être enfilés. Je suppose que cela fournit également une méthode de mesure efficace du changement de contexte. Vérifiez combien de temps le tableau (dans l'exemple supérieur) doit être afin que deux threads du pool de threads commencent à montrer un réel avantage par rapport à un seul thread. Cela peut facilement devenir 100 000 éléments et, par conséquent, l'heure de changement de contexte effective se situerait quelque part dans la plage de 20us au sein de la même application.

Toutes les encapsulations utilisées par le pool de threads doivent être comptées jusqu'à l'heure de changement de thread car c'est à cela que tout se résume (à la fin).

Atmapuri

3
Atmapuri

Le changement de contexte est très coûteux. Pas à cause du fonctionnement du CPU lui-même, mais à cause de l'invalidation du cache. Si vous avez une tâche intensive en cours d'exécution, elle remplira le cache du processeur, à la fois pour les instructions et les données, la pré-lecture de la mémoire, TLB et RAM optimisera le travail vers certaines zones de ram.

Lorsque vous changez de contexte, tous ces mécanismes de cache sont réinitialisés et le nouveau thread démarre à partir de l'état "vide".

La réponse acceptée est fausse, sauf si votre thread incrémente simplement un compteur. Bien sûr, il n'y a pas de vidage de cache impliqué dans ce cas. Il est inutile de comparer le changement de contexte sans remplir le cache comme les applications réelles.

1
bokan

Je ne sais pas, mais avez-vous les compteurs de performances habituels dans Windows Mobile? Vous pouvez regarder des choses comme les changements de contexte/sec. Je ne sais pas s'il y en a un qui mesure spécifiquement l'heure de changement de contexte.

1
1800 INFORMATION