web-dev-qa-db-fra.com

Quel est le surcoût d'un changement de contexte?

À l'origine, je pensais que les frais généraux d'un changement de contexte étaient le TLB en cours de vidage. Cependant je viens de voir sur wikipedia:

http://en.wikipedia.org/wiki/Translation_lookaside_buffer

En 2008, Intel (Nehalem) [18] et AMD (SVM) [19] ont introduit des balises dans le cadre de l'entrée TLB et un matériel dédié qui vérifie la balise lors de la recherche. Même si celles-ci ne sont pas pleinement exploitées, il est envisagé qu'à l'avenir, ces balises identifieront l'espace d'adressage auquel appartient chaque entrée TLB. Ainsi, un changement de contexte n'entraînera pas le vidage du TLB - mais changera simplement la balise de l'espace d'adressage actuel en balise de l'espace d'adressage de la nouvelle tâche.

Est-ce que ce qui précède confirme pour les nouveaux processeurs Intel que le TLB n'est pas vidé sur les commutateurs de contexte?

Cela signifie-t-il qu'il n'y a pas vraiment de surcharge dans un changement de contexte?

(J'essaie de comprendre la pénalité de performance d'un changement de contexte)

23
user997112

Comme wikipedia le sait dans son commutateur de contexte article, " le commutateur de contexte est le processus de stockage et de restauration de l'état (contexte) d'un processus afin que l'exécution puisse être reprise à partir du même point à un plus tard. ". Je suppose que le changement de contexte entre deux processus du même système d'exploitation, pas la transition en mode utilisateur/noyau (syscall), qui est beaucoup plus rapide et ne nécessite pas de vidage TLB.

Ainsi, le noyau du système d'exploitation a besoin de beaucoup de temps pour enregistrer l'état d'exécution (tous, vraiment tous, les registres; et de nombreuses structures de contrôle spéciales) du processus en cours d'exécution dans la mémoire, puis charger l'état d'exécution d'un autre processus (lu dans la mémoire) . Le rinçage TLB, si nécessaire, ajoutera du temps au commutateur, mais il ne représente qu'une petite partie de la surcharge totale.

Si vous voulez trouver la latence de changement de contexte, il existe lmbench outil de référence http://www.bitmover.com/lmbench/ avec test LAT_CTX http: // www .bitmover.com/lmbench/lat_ctx.8.html

Je ne peux pas trouver de résultats pour nehalem (y a-t-il lmbench dans la suite phoronix?), Mais pour core2 et Linux moderne le changement de contexte peut coûter 5-7 microsecondes.

Il existe également des résultats pour un test de qualité inférieure http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html avec 1- 3 microsecondes pour le changement de contexte. Impossible d'obtenir l'effet exact du non rinçage du TLB à partir de ses résultats.

[~ # ~] mise à jour [~ # ~] - Votre question devrait porter sur la virtualisation, pas sur le changement de contexte de processus.

RWT dit dans leur article sur Nehalem "Inside Nehalem: Intel's Future Processor and System. TLBs, Page Tables and Synchronization" 2 avril 2008 par David Kanter, que Nehalem a ajouté VPID au TLB pour créer une machine virtuelle/Commutateurs hôtes (vmentry/vmexit) plus rapides:

Les entrées TLB de Nehalem ont également changé subtilement en introduisant un "ID de processeur virtuel" ou VPID. Chaque entrée TLB met en cache une traduction d'adresse virtuelle en adresse physique ... cette traduction est spécifique à un processus et à une machine virtuelle donnés. Les processeurs plus anciens d'Intel vidaient les TLB chaque fois que le processeur basculait entre l'invité virtualisé et l'instance hôte, pour garantir que les processus accédaient uniquement à la mémoire qu'ils étaient autorisés à toucher. Le VPID suit à quel VM une entrée de traduction donnée dans le TLB est associée, de sorte que lorsqu'une VM sortie et rentrée se produit, les TLB ne doivent être vidées pour des raisons de sécurité. .... Le VPID est utile pour les performances de virtualisation en abaissant la surcharge de transitions VM; Intel estime que la latence d'un aller-retour VM à Nehalem est de 40% par rapport à Merom (c'est-à-dire le 65 nm Core 2) et environ un tiers de moins que le 45 nm Penryn.

De plus, vous devez savoir que dans le fragment cité par vous dans la question, le lien "[18]" était "G. Neiger, A. Santoni, F. Leung, D. Rodgers et R. Uhlig. Intel Virtualization Technologie: Prise en charge matérielle d'une virtualisation efficace des processeurs . Intel Technology Journal, 10 (3). ", Il s'agit donc d'une fonctionnalité de virtualisation efficace (hôte invité rapide interrupteurs).

19
osgx

Si nous comptons dans l'invalidation du cache (ce que nous devrions habituellement, et qui est le plus grand contributeur aux coûts de changement de contexte dans le monde réel), la pénalité de performance due au changement de contexte peut être ÉNORME:

https://www.usenix.org/legacy/events/expcs07/papers/2-li.pdf (certes un peu dépassé, mais le meilleur que j'ai pu trouver) le donne dans la gamme de 100K-1M cycles de CPU. Théoriquement, dans le pire des cas pour un boîtier de serveur multi-socket avec 32M de cache L3 par socket, composé de lignes de cache de 64 octets, un accès complètement aléatoire et des temps d'accès typiques de 40 cycles pour L3/100 cycles pour la RAM principale , la pénalité peut atteindre jusqu'à 30 millions de cycles CPU (!).

D'après mon expérience personnelle, je dirais qu'il est généralement de l'ordre de dizaines de cycles K, mais en fonction des spécificités, il peut différer d'un ordre de grandeur.

8
No-Bugs Hare

Décomposons le coût d'un commutateur de tâches en "coûts directs" (le coût du code de commutateur de tâches lui-même) et en "coûts indirects" (le coût des échecs TLB, etc.).

Coûts directs

Pour les coûts directs, il s'agit principalement du coût de l'enregistrement de l'état (visible sur le plan de l'architecture pour l'espace utilisateur) pour la tâche précédente, puis du chargement de l'état pour la tâche suivante. Cela varie en fonction de la situation, principalement parce qu'il peut ou non inclure un état FPU/MMX/SSE/AVX qui peut ajouter jusqu'à plusieurs Ko de données (en particulier si AVX est impliqué - par exemple AVX2 fait 512 octets par lui-même et AVX- 512 est supérieur à 2 Kio seul).

Notez qu'il existe un mécanisme de "chargement d'état paresseux" pour éviter le coût de chargement (en partie ou en totalité) de l'état FPU/MMX/SSE/AVX et éviter le coût de l'enregistrement de cet état s'il n'était pas chargé; et cette fonctionnalité peut être désactivée pour des raisons de performances (si presque toutes les tâches utilisent l'état, le coût d'un piège/exception "l'état est utilisé doit être chargé" dépasse ce que vous économisez en essayant d'éviter de le faire pendant le changement de tâche) ou pour des raisons de sécurité (par exemple parce que le code sous Linux "enregistre s'il est utilisé" et non "enregistre puis efface si utilisé" et laisse les données appartenant à une tâche dans des registres qui peuvent être obtenus par une autre tâche via des attaques d'exécution spéculative).

Il existe également d'autres coûts (mise à jour des statistiques - par exemple, "quantité de temps CPU utilisé par la tâche précédente"), déterminant si la nouvelle tâche utilise le même espace d'adressage virtuel que l'ancienne tâche (par exemple, un thread différent dans le même processus), etc.

Coûts indirects

Les coûts indirects sont essentiellement la perte d'efficacité pour toutes les choses "similaires au cache" du CPU - les caches eux-mêmes, les TLB, les caches de structure de pagination de niveau supérieur, tous les trucs de prédiction de branche (direction de branche, cible de branche, tampon de retour), etc. .

Les coûts indirects peuvent être divisés en 3 causes. L'un est les coûts indirects qui surviennent parce que la chose a été complètement vidée par le commutateur de tâches. Dans le passé, cela était principalement limité aux échecs TLB causés parce que les TLB étaient vidés lors du changement de tâche. Notez que cela peut se produire même lorsque PCID est utilisé - il y a une limite de 4096 ID (et lorsque "l'atténuation de la fusion" est utilisée, les ID sont utilisés par paires - pour chaque espace d'adressage virtuel, un ID est utilisé pour l'espace utilisateur et un autre pour le noyau), ce qui signifie que lorsqu'il y a plus de 4096 (ou 2048) espaces d'adressage virtuels utilisés, le noyau doit recycler les ID précédemment utilisés et vider tous les TLB pour l'ID qui est réutilisé. Cependant, maintenant (avec tous les problèmes de sécurité d'exécution spéculative), le noyau peut vider d'autres choses (par exemple, des trucs de prédiction de branche) afin que les informations ne puissent pas fuir d'une tâche à une autre, mais je ne sais vraiment pas si Linux le fait ou ne le fait pas. Je ne supporte pas cela pour des choses "comme le cache" (et je soupçonne qu'ils essaient principalement d'empêcher les fuites de données du noyau vers l'espace utilisateur et finissent par empêcher les données de fuir d'une tâche à une autre par accident).

Une autre cause de coûts indirects est les limites de capacité. Par exemple, si le cache L2 ne peut mettre en cache qu'un maximum de 256 Ko de données et que la tâche précédente a utilisé plus de 256 Ko de données; alors le cache L2 sera plein de données inutiles pour la prochaine tâche et toutes les données que la prochaine tâche veut mettre en cache (et qui avaient précédemment été mises en cache) auront été expulsées en raison de la "dernière utilisation". Cela s'applique à toutes les choses "de type cache" (y compris les TLB et les caches de structure de pagination de niveau supérieur, même lorsque la fonctionnalité PCID est utilisée).

L'autre cause des coûts indirects est la migration d'une tâche vers un autre processeur. Cela dépend des CPU - par exemple si la tâche est migrée vers un autre processeur logique au sein du même noyau, une grande partie des éléments "similaires au cache" peuvent être partagés par les deux processeurs et les coûts de migration peuvent être relativement faibles; et si la tâche est migrée vers un CPU dans un package physique différent, aucune des choses "de type cache" ne peut être partagée par les deux CPU et les coûts de migration peuvent être relativement importants.

Notez que la limite supérieure de l'ampleur des coûts indirects dépend de ce que fait la tâche. Par exemple, si une tâche utilise une grande quantité de données, les coûts indirects peuvent être relativement chers (beaucoup de cache et TLB manquent), et si la tâche utilise une petite quantité de données, les coûts indirects peuvent être négligeables (très peu de cache et TLB manque).

Sans rapport

Notez que la fonctionnalité PCID a ses propres coûts (non liés aux commutateurs de tâches eux-mêmes). Plus précisément; lorsque les traductions de page sont modifiées sur un processeur, elles peuvent avoir besoin d'être invalidées sur d'autres processeurs à l'aide de ce que l'on appelle un "arrêt TLB multi-CPU", ce qui est relativement coûteux (implique une interruption IPI/inter-processeurs qui perturbe les autres CPU et coûte "des centaines faibles" de cycles "par CPU). Sans PCID, vous pouvez éviter certains d'entre eux. Par exemple, sans PCID, pour un processus à un seul thread qui s'exécute sur un processeur, vous savez qu'aucun autre processeur ne peut utiliser le même espace d'adressage virtuel et savez donc que vous n'avez pas besoin de faire le "shootdown TLB multi-CPU" ", et si un processus multithread est limité à un seul domaine NUMA, alors seuls les processeurs de ce domaine NUMA doivent être impliqués dans le" shootdown TLB multi-CPU ". Lorsque PCID est utilisé, vous ne pouvez pas compter sur ces astuces et avoir des frais généraux plus élevés, car le "shootdown TLB multi-CPU" n'est pas évité aussi souvent.

Bien sûr, il y a aussi des coûts associés à la gestion des ID (par exemple, déterminer quel ID est libre d'attribuer à une tâche nouvellement créée, révoquer les ID lorsque les tâches sont terminées, une sorte de système "le moins récemment utilisé" pour réaffecter les ID quand il y en a plus des espaces d'adressage virtuels que des ID, etc.).

En raison de ces coûts, il y a forcément des cas pathologiques où le coût d'utilisation de PCID dépasse les avantages "moins de TLB manqués causés par les changements de tâches" (où l'utilisation de PCID aggrave les performances).

5
Brendan

Remarque: comme Brendan l'a souligné dans son commentaire. L'objectif de cette réponse est de répondre au détail Quel est l'impact global des changements de contexte sur les performances d'un serveur/bureau Windows, y compris les frais généraux du système d'exploitation qui sont différents sous Windows vs Linux vs Solaris etc ... =.

La meilleure façon de le savoir est bien sûr de le comparer. Le problème ici est que la relation entre le nombre de changements de contexte par seconde et le temps CPU est exponentielle. En d'autres termes, c'est un O (n2) Coût. Cela signifie que nous avons une limite maximale qui ne peut tout simplement pas être dépassée.

Le code de référence suivant utilise quelques variables dangereuses, etc ... ignorez cela car ce n'est pas le point.

Le travail réel effectué par thread est minime. En théorie, chaque thread devrait générer 1000 changements de contexte par seconde.

  • Vider le code suivant dans une application de console .NET et voir les résultats dans perfmon.
  • Ajoutez deux compteurs à Perfmon: Processeur ->% temps processeur et Système-> Commutateurs de contexte par seconde . Sur une machine à 8 cœurs, 128 threads génèrent environ 0,1% de surcharge CPU à partir du travail effectué par les threads.

Il semblerait que 2560 threads devraient générer environ 2% de CPU, mais que le CPU passe à 100% à 2300 threads (sur ma machine de bureau Core i7-4790K 4 Core + 4 hyperthreaded Core).

  • 2048 threads - 2 millions de changements de contexte par seconde: CPU à 40%
  • 2300 threads - 2,3 millions de changements de contexte par seconde: CPU à 100%

perfmon graph

static void Main(string[] args)
{
    ThreadTestClass ThreadClass;
    bool Wait;
    int Counter;
    Wait = true;
    Counter = 0;
    while (Wait)
    {
        if (Console.KeyAvailable)
        {
            ConsoleKey Key = Console.ReadKey().Key;
            switch (Key)
            {
                case ConsoleKey.UpArrow:
                    ThreadClass = new ThreadTestClass();
                    break;
                case ConsoleKey.DownArrow:
                    SignalExitThread();
                    break;
                case ConsoleKey.PageUp:
                    SleepTime += 1;
                    break;
                case ConsoleKey.PageDown:
                    SleepTime -= 1;
                    break;
                case ConsoleKey.Insert:
                    for (int I = 0; I < 64; I++)
                    {
                        ThreadClass = new ThreadTestClass();
                    }
                    break;
                case ConsoleKey.Delete:
                    for (int I = 0; I < 64; I++)
                    {
                        SignalExitThread();
                    }
                    break;
                case ConsoleKey.Q:
                    Wait = false;
                    break;
                case ConsoleKey.Spacebar:
                    Wait = false;
                    break;
                case ConsoleKey.Enter:
                    Wait = false;
                    break;
            }
        }
        Counter += 1;
        if (Counter >= 10)
        {
            Counter = 0;
            Console.WriteLine(string.Concat(@"Thread Count: ", NumThreadsActive.ToString(), @" - SleepTime: ", SleepTime.ToString(), @" - Counter: ", UnSafeCounter.ToString()));
        }
        System.Threading.Thread.Sleep(100);
    }
    IsActive = false;
}

public static object SyncRoot = new object();
public static bool IsActive = true;
public static int SleepTime = 1;
public static long UnSafeCounter = 0;
private static int m_NumThreadsActive;
public static int NumThreadsActive
{
    get
    {
        lock(SyncRoot)
        {
            return m_NumThreadsActive;
        }
    }
}
private static void NumThreadsActive_Inc()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive += 1;
    }
}
private static void NumThreadsActive_Dec()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive -= 1;
    }
}
private static int ThreadsToExit = 0;
private static bool ThreadExitFlag = false;
public static void SignalExitThread()
{
    lock(SyncRoot)
    {
        ThreadsToExit += 1;
        ThreadExitFlag = (ThreadsToExit > 0);
    }
}

private static bool ExitThread()
{
    if (ThreadExitFlag)
    {
        lock (SyncRoot)
        {
            ThreadsToExit -= 1;
            ThreadExitFlag = (ThreadsToExit > 0);
            return (ThreadsToExit >= 0);
        }
    }
    return false;
}

public class ThreadTestClass
{
    public ThreadTestClass()
    {
        System.Threading.Thread RunThread;
        RunThread = new System.Threading.Thread(new System.Threading.ThreadStart(ThreadRunMethod));
        RunThread.Start();
    }

    public void ThreadRunMethod()
    {
        long Counter1;
        long Counter2;
        long Counter3;
        Counter1 = 0;
        NumThreadsActive_Inc();
        try
        {
            while (IsActive && (!ExitThread()))
            {
                UnSafeCounter += 1;
                System.Threading.Thread.Sleep(SleepTime);
                Counter1 += 1;
                Counter2 = UnSafeCounter;
                Counter3 = Counter1 + Counter2;
            }
        }
        finally
        {
            NumThreadsActive_Dec();
        }
    }
}
0
tcwicks