60 votes

Comment estimer le surcoût lié au changement de contexte des threads ?

J'essaie d'améliorer les performances de l'application threadée avec des échéances en temps réel. Elle fonctionne sur Windows Mobile et est écrite en C / C++. J'ai le sentiment que la fréquence élevée de changement de threads pourrait causer une surcharge tangible, mais je ne peux ni le prouver ni l'infirmer. Comme chacun sait, l'absence de preuve n'est pas une preuve du contraire :).

Ma question est donc double :

  • Si elle existe, où puis-je trouver des mesures réelles du coût du changement de contexte de fil ?

  • 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 commutations de contexte (on / off) pour un thread donné ?

4 votes

Je pense que le changement de thread dépend fortement de la quantité de "mémoire" et d'état qu'un seul thread "contient". Si tous vos threads font beaucoup de travail sur des bitmaps énormes, un changement de thread peut être très coûteux. Un thread qui ne fait qu'incrémenter un seul compteur a une très faible surcharge de changement de thread.

0 votes

La réponse acceptée est fausse. Le changement de contexte est coûteux à cause de l'invalidation du cache. Bien sûr, si vous évaluez uniquement le changement de thread avec un incrément de compteur, cela semble rapide, mais c'est une évaluation irréaliste et sans valeur. Ce n'est même pas vraiment un changement de contexte quand le contexte est juste le registre du compteur.

28voto

Mecki Points 35351

Je doute que vous puissiez trouver ces frais généraux quelque part sur le web pour une quelconque plateforme existante. Il existe simplement trop de plateformes différentes. L'overhead dépend de deux facteurs :

  • L'unité centrale de traitement (UC), car les opérations nécessaires peuvent être plus faciles ou plus difficiles sur différents types d'UC.
  • Le noyau du système, car des noyaux différents devront effectuer des opérations différentes sur chaque commutateur.

Parmi les autres facteurs, citons la manière dont le changement s'opère. Une commutation peut avoir lieu lorsque

  1. le fil a utilisé tous ses quantum de temps. Lorsqu'un thread est lancé, il peut fonctionner pendant un temps donné avant de devoir rendre le contrôle au noyau qui décidera de la suite.

  2. le thread 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 l'entrée de la souris/du clavier peut être un tel thread. Quel que soit le thread possède l'unité centrale en ce moment, lorsque l'utilisateur tape ou clique sur quelque chose, il ne veut pas attendre que le quantum de temps du thread en cours soit complètement utilisé, il veut voir le système réagir immédiatement. C'est pourquoi certains systèmes arrêtent immédiatement le thread en cours et redonnent le contrôle à un autre thread ayant une priorité plus élevée.

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

Ces 3 scénarios pourraient avoir des temps de commutation de fils 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 CPU est rendu au noyau et que le noyau doit mettre en place un appel de réveil qui s'assurera que le thread est réveillé après environ le temps qu'il a demandé à dormir, il doit ensuite retirer le thread du processus de planification, et une fois que le thread est réveillé, il doit ajouter le thread à nouveau au processus de planification. Toutes ces étapes prennent 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 en être sûr, vous devez faire des tests de référence. Le problème est que vous devrez généralement soit mettre les threads en sommeil, soit les synchroniser en utilisant des mutex. La mise en sommeil ou le verrouillage/déverrouillage des mutex a en soi une surcharge. Cela signifie que votre benchmark devra également inclure 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 switch réel et combien pour le sleep/mutex-call. D'autre part, dans un scénario de la vie réelle, vos threads vont soit dormir, soit se synchroniser via des verrous. Un benchmark qui mesure uniquement le temps de changement de contexte est un benchmark synthétique car il ne modélise aucun scénario réel. Les benchmarks sont beaucoup plus "réalistes" s'ils se basent 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 faire gérer par le GPU par seconde ?

Malheureusement, je ne connais rien à la programmation Windows. Je pourrais écrire une application pour Windows en Java ou peut-être en C#, mais le C/C++ sur Windows me fait pleurer. Je peux seulement vous offrir 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;
}

Sortie

Number of thread switches in about one second was 108406

Plus de 100'000, ce n'est pas si mal et ce, même si nous avons des verrouillages et des attentes conditionnelles. Je pense que sans tout cela, il y aurait au moins deux fois plus de changements de threads par seconde.

16 votes

Quelle partie de "Malheureusement, je ne connais rien à la programmation Windows... je peux seulement vous offrir du code source pour POSIX." n'avez-vous pas compris ?

6 votes

Non, je comprends parfaitement, mais votre réponse n'aide pas celui qui a posé la question initiale, alors que le but est d'aider ceux qui posent des questions.

14voto

ctacke Points 53946

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

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

Tout d'abord, la manière de coder (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));
}

Il est évident qu'il est préférable de le faire en boucle et de calculer la moyenne. 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 va immédiatement basculer vers 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 programmateur, mais c'est loin d'être simple à faire et ce n'est pas très bien documenté. Si vous voulez vraiment suivre cette voie, Sue Loh a plusieurs blogs à ce sujet qu'un moteur de recherche peut trouver.

La voie non codée 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 du curseur fourni. Encore une fois, je suis certain que Sue a un article de blog sur l'utilisation de Kernel Tracker aussi.

Cela dit, vous allez constater que les changements de contexte de threads intra-processus de l'EC sont vraiment, vraiment rapides. Ce sont les commutations de processus qui sont coûteuses, car elles nécessitent de permuter le processus actif en RAM, puis de procéder à la migration.

12voto

OregonGhost Points 16615

Bien que vous ayez 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 Linux ARM9 afin de déterminer l'overhead. Il s'agissait simplement de deux threads qui boostaient::thread::yield() (ou, vous savez) et incrémentaient une variable, et après une minute environ (sans autres processus en cours, du moins aucun qui fasse quelque chose), l'application a imprimé le nombre de changements de contexte qu'elle pouvait faire par seconde. Bien sûr, ce n'est pas vraiment exact, mais le fait est que les deux threads se sont cédés le CPU l'un à l'autre, et c'était si rapide que cela n'avait plus de sens de penser à l'overhead. Donc, allez-y simplement et écrivez un test simple au lieu de trop penser à un problème qui pourrait être inexistant.

Sinon, vous pouvez essayer comme 1800 l'a suggéré avec les compteurs de performance.

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

2 votes

Merci, cette affirmation que les temps de commutation sont minimes est ce dont j'avais besoin.

2 votes

Il est inutile d'évaluer la commutation de contexte avec des processus qui n'utilisent pas la mémoire cache.

7voto

bobah Points 7375

Mon 50 lignes de C++ montre pour Linux (QuadCore Q6600) le temps de changement de contexte ~ 0.9us (0.75us pour 2 threads, 0.95 pour 50 threads). Dans ce benchmark les threads appellent le rendement immédiatement quand ils obtiennent un quantum de temps.

3 votes

.9 NANOSECONDS ? Vous êtes sûr ? ... <rummages...> votre code semble calculer des miillisecondes/switch*1000-> microsecondes.

0 votes

@IraBaxter ce n'est pas une nano-seconde, 1000us==1ms 1000ms==1s

0 votes

Plus de 1000 commutateurs par milliseconde ?? Vous êtes sûr ?

5voto

Tim Ring Points 970

Je n'ai essayé qu'une seule fois d'estimer cela et c'était sur un 486 ! Le résultat était que le changement de contexte du processeur prenait environ 70 instructions pour se terminer (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 changement de thread (y compris l'overhead du système d'exploitation) sur un DX3. Les quelques milliers de commutations de contexte que nous faisions par seconde absorbaient entre 5 et 10% du temps du processeur.

Je ne sais pas comment cela se traduirait sur un processeur moderne à plusieurs cœurs et plusieurs GHz, mais je pense qu'à moins d'aller jusqu'à l'excès avec la commutation de threads, il s'agit d'une surcharge négligeable.

Notez que la création/suppression de fils est plus coûteuse en termes de CPU/OS que l'activation/désactivation de fils. Une bonne politique pour les applications fortement threadées consiste à utiliser des pools de threads et à les activer/désactiver selon les besoins.

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