LA CLOCHE

Il y a ceux qui ont lu cette nouvelle avant vous.
Abonnez-vous pour recevoir les derniers articles.
Email
Nom
Nom de famille
Comment voulez-vous lire The Bell
Pas de spam

saul 9 septembre 2015 à 13:38

Implémentation d'une architecture de moteur de jeu multi-thread

  • Blog Intel,
  • Développement de jeu,
  • Programmation parallèle,
  • Développement de site Web
  • Transfert

Avec l'avènement des processeurs multicœurs, il est devenu nécessaire de créer un moteur de jeu basé sur une architecture parallèle. L'utilisation de tous les processeurs du système - à la fois le processeur graphique (GPU) et le processeur central (CPU) - ouvre beaucoup plus de possibilités par rapport à un moteur monothread basé sur GPU uniquement. Par exemple, l'utilisation de plus de cœurs de processeur peut améliorer les visuels en augmentant le nombre d'objets physiques utilisés dans le jeu, ainsi que parvenir à un comportement de personnage plus réaliste grâce à la mise en œuvre d'une intelligence artificielle avancée (IA).
Considérons les fonctionnalités d'implémentation de l'architecture multithread du moteur de jeu.

1. Introduction

1.1. Aperçu

L'architecture multithread du moteur de jeu vous permet d'utiliser au maximum les capacités de tous les processeurs de la plateforme. Il suppose une exécution parallèle de divers blocs fonctionnels sur tous les processeurs disponibles. Cependant, il n’est pas si facile de mettre en œuvre un tel système. Les éléments individuels du moteur de jeu interagissent souvent les uns avec les autres, ce qui peut entraîner des erreurs lorsqu'ils sont exécutés simultanément. Pour gérer de tels scénarios, le moteur fournit des mécanismes spéciaux pour la synchronisation des données, à l'exclusion des verrous possibles. Il met également en œuvre des méthodes pour synchroniser les données en même temps, minimisant ainsi le temps d'exécution.

Pour comprendre le matériel présenté, vous devez bien connaître les méthodes modernes de création de jeux informatiques, la prise en charge du multithreading pour les moteurs de jeu ou l'amélioration des performances des applications en général.

2. État d'exécution parallèle

L'état de concurrence est un concept clé en multithreading. Ce n'est qu'en divisant le moteur de jeu en systèmes séparés, chacun fonctionnant dans son propre mode et n'interagissant pratiquement pas avec le reste du moteur, que l'on peut atteindre la plus grande efficacité du calcul parallèle et réduire le temps nécessaire à la synchronisation. Il n'est pas possible d'isoler complètement des pièces individuelles du moteur en excluant toutes les ressources partagées. Cependant, pour des opérations telles que l'obtention de données de position ou d'orientation pour des objets, les systèmes individuels peuvent utiliser des copies locales des données plutôt que des ressources partagées. Cela vous permet de minimiser la dépendance des données dans différentes parties du moteur. Les modifications apportées aux données partagées par un système individuel sont notifiées au gestionnaire d'état, qui les met en file d'attente. C'est ce qu'on appelle le mode de messagerie. Ce mode suppose qu'après avoir terminé les tâches, les systèmes du moteur reçoivent des notifications de modifications et mettent à jour leurs données internes en conséquence. Ce mécanisme peut réduire considérablement le temps de synchronisation et la dépendance des systèmes les uns par rapport aux autres.

2.1 États d'exécution

Pour que le gestionnaire d'état d'exécution fonctionne efficacement, il est recommandé de synchroniser les opérations sur une horloge spécifique. Cela permet à tous les systèmes de fonctionner simultanément. Cependant, la fréquence d'horloge ne doit pas nécessairement correspondre à la fréquence d'images. Et la durée des cycles d'horloge peut ne pas dépendre de la fréquence. Il peut être sélectionné de manière à ce qu'un cycle corresponde au temps nécessaire pour transmettre une trame (quelle que soit sa taille). En d'autres termes, la fréquence ou la durée des ticks est déterminée par l'implémentation spécifique du gestionnaire d'état. La figure 1 montre un "gratuit" mode pas à pas travail qui n'exige pas que tous les systèmes exécutent une opération dans le même cycle d'horloge. Le mode dans lequel tous les systèmes terminent l'exécution des opérations en un cycle d'horloge est appelé mode pas à pas "dur". Il est représenté schématiquement sur la figure 2.


Figure 1. État d'exécution en mode étape par étape libre

2.1.1. Mode étape par étape gratuit
Dans un mode pas à pas libre, tous les systèmes fonctionnent en continu pendant une période de temps prédéterminée nécessaire pour terminer la partie suivante des calculs. Cependant, le nom «libre» ne doit pas être pris au pied de la lettre: les systèmes ne sont pas synchronisés à un moment arbitraire dans le temps, ils sont seulement «libres» dans le choix du nombre de cycles d'horloge nécessaires pour terminer l'étape suivante.
En règle générale, dans ce mode, il ne suffit pas d'envoyer une simple notification de changement d'état au gestionnaire d'état. Les données mises à jour doivent également être envoyées. Cela est dû au fait que le système qui a modifié les données partagées peut être en état de fonctionnement alors qu'un autre système en attente de ces données est prêt à être mis à jour. Dans ce cas, plus de mémoire est nécessaire car davantage de copies des données doivent être effectuées. Par conséquent, le régime «libre» ne peut être considéré comme une solution universelle pour toutes les occasions.
2.1.2. Mode étape difficile
Dans ce mode, l'exécution des tâches sur tous les systèmes est effectuée en un seul cycle. Ce mécanisme est plus simple à mettre en œuvre et ne nécessite pas le transfert de données mises à jour avec la notification. En effet, si nécessaire, un système peut simplement demander de nouvelles valeurs à un autre système (bien sûr, à la fin du cycle d'exécution).
En mode difficile, vous pouvez implémenter un mode de fonctionnement pas à pas pseudo-libre, répartissant les calculs entre différentes étapes. En particulier, cela peut être nécessaire pour les calculs AI, où un "objectif commun" initial est calculé dans le premier cycle d'horloge, qui est progressivement affiné dans les étapes suivantes.


Figure 2. État d'exécution en mode étape par étape difficile

2.2. Synchronisation des données

Les modifications apportées aux données partagées par plusieurs systèmes peuvent entraîner un conflit de modifications. Dans ce cas, le système de messagerie doit fournir un algorithme pour choisir la valeur totale correcte. Il existe deux approches principales basées sur les critères suivants.
  • Heure: la valeur finale correspond au dernier changement effectué.
  • Priorité: Le total est le changement effectué par le système avec la priorité la plus élevée. Si la priorité des systèmes est la même, vous pouvez également prendre en compte l'heure du changement.
Toutes les données obsolètes (pour l'un des critères) peuvent être simplement écrasées ou exclues de la file d'attente de notification.
Étant donné que le total peut varier en fonction de l'ordre dans lequel les modifications sont apportées, il peut être très difficile d'utiliser les valeurs relatives des données totales. Dans de tels cas, des valeurs absolues doivent être utilisées. Ensuite, lors de la mise à jour des données locales, les systèmes peuvent simplement remplacer les anciennes valeurs par de nouvelles. La solution optimale est de choisir des valeurs absolues ou relatives en fonction de la situation spécifique. Par exemple, les données générales telles que la position et l'orientation doivent être absolues car l'ordre dans lequel les modifications sont apportées est important. Les valeurs relatives peuvent être utilisées, par exemple, pour un système de génération de particules, car toutes les informations sur les particules y sont stockées uniquement.

3. Moteur

Lors du développement du moteur, l'objectif principal est la flexibilité requise pour étendre encore ses fonctionnalités. Cela l'optimisera pour une utilisation sous certaines contraintes (ex: mémoire).
Le moteur peut être divisé en deux parties: le cadre et les gestionnaires. Le framework (voir section 3.1) comprend des parties du jeu qui sont répliquées à l'exécution, c'est-à-dire qu'elles existent dans plusieurs instances. Il comprend également les éléments impliqués dans l'exécution de la boucle de jeu principale. Les managers (voir section 3.2) sont des objets Singleton chargés d'exécuter la logique du jeu.
Voici un diagramme du moteur de jeu.


Figure 3. Architecture générale du moteur

Veuillez noter que les modules ou systèmes de jeu fonctionnels ne font pas partie du moteur. Le moteur ne fait que les réunir, agissant comme un élément de liaison. Cette organisation modulaire permet de charger et décharger les systèmes selon les besoins.

L'interaction du moteur et des systèmes est réalisée à l'aide d'interfaces. Ils sont mis en œuvre de manière à fournir au moteur un accès aux fonctions du système et aux systèmes - aux gestionnaires de moteur.
Un diagramme détaillé du moteur est fourni à l'annexe A, Diagramme du moteur.

Pratiquement tous les systèmes sont indépendants les uns des autres (voir Section 2, «État d'exécution simultanée»), c'est-à-dire qu'ils peuvent effectuer des actions en parallèle sans affecter le fonctionnement des autres systèmes. Cependant, tout changement de données entraînera certaines difficultés, car les systèmes devront interagir les uns avec les autres. L'échange d'informations entre les systèmes est nécessaire dans les cas suivants:

  • pour informer un autre système d'un changement de données générales (par exemple, la position ou l'orientation des objets);
  • pour exécuter des fonctions qui ne sont pas disponibles pour ce système (par exemple, le système AI se tourne vers le système de calcul des propriétés géométriques ou physiques d'un objet pour effectuer un test d'intersection de rayons).
Dans le premier cas, le gestionnaire d'état décrit dans la section précédente peut être utilisé pour contrôler l'échange d'informations. (Pour plus d'informations sur le State Manager, reportez-vous à la Section 3.2.2, «State Manager».)
Dans le second cas, il est nécessaire de mettre en œuvre un mécanisme spécial qui fournira les services d'un système pour l'utilisation d'un autre. Description complète ce mécanisme est décrit dans la Section 3.2.3, «Gestionnaire de service».

3.1. Cadre

Le cadre est utilisé pour combiner tous les éléments du moteur. C'est là que le moteur est initialisé, à l'exception des gestionnaires, qui sont instanciés globalement. Il stocke également des informations sur la scène. Pour obtenir une plus grande flexibilité, la scène est implémentée comme une scène dite générique contenant des objets génériques. Ce sont des conteneurs qui combinent diverses parties fonctionnelles de la scène. Pour plus de détails, voir la section 3.1.2.
La boucle principale du jeu est également implémentée dans le framework. Elle peut être représentée schématiquement comme suit.


Figure 4. Boucle de jeu principale

Le moteur fonctionne dans un environnement fenêtré, donc la première étape de la boucle de jeu doit traiter tous les messages de fenêtre OS inachevés. Si cela n'est pas fait, le moteur ne répondra pas aux messages du système d'exploitation. Dans la deuxième étape, le planificateur attribue des tâches à l'aide du gestionnaire de tâches. Ce processus est détaillé dans la section 3.1.1 ci-dessous. Après cela, le gestionnaire d'état (voir section 3.2.2) envoie des informations sur les modifications apportées aux systèmes du moteur, dont le travail peut affecter. À la dernière étape, en fonction de l'état de l'exécution, le cadre détermine s'il faut arrêter ou continuer le moteur, par exemple pour passer à la scène suivante. Les informations sur l'état du moteur sont stockées par le gestionnaire d'environnement. Pour plus de détails, voir la section 3.2.4.

3.1.1. Planificateur
Le planificateur génère une horloge de référence d'exécution à une fréquence spécifiée. Si le mode de référence nécessite que l'opération suivante démarre immédiatement après l'achèvement de la précédente, sans attendre la fin de l'horloge, la fréquence peut être illimitée.
Sur un signal d'horloge, le planificateur utilise le gestionnaire de tâches pour mettre les systèmes en mode exécution. En mode pas à pas libre (section 2.1.1), le planificateur interroge les systèmes pour déterminer le nombre de cycles d'horloge dont ils auront besoin pour accomplir la tâche. Sur la base des résultats de l'enquête, l'ordonnanceur détermine quels systèmes sont prêts à être exécutés et lesquels termineront leur travail à un moment donné. Le planificateur peut modifier le nombre de graduations si un système prend plus de temps à s'exécuter. En mode pas à pas difficile (Section 2.1.2), tous les systèmes démarrent et finissent au même cycle d'horloge, de sorte que le planificateur attend que tous les systèmes aient fini de s'exécuter.
3.1.2. Scène et objets polyvalents
La scène et les objets génériques sont des conteneurs pour les fonctionnalités implémentées dans d'autres systèmes. Ils sont uniquement destinés à interagir avec le moteur et n'exécutent aucune autre fonction. Cependant, ils peuvent être étendus pour tirer parti des fonctions disponibles sur d'autres systèmes. Cela permet un couplage faible. En effet, une scène et des objets universels peuvent utiliser les propriétés d'autres systèmes sans y être liés. C'est cette propriété qui élimine la dépendance des systèmes les uns sur les autres et leur permet de fonctionner simultanément.
Le schéma ci-dessous montre une extension d'une scène et d'un objet universels.


Figure 5. Extension de la scène et de l'objet universels

Voyons comment les extensions fonctionnent dans l'exemple suivant. Supposons que l'extension de la scène universelle soit effectuée et que la scène soit étendue pour utiliser les propriétés graphiques, physiques et autres. Dans ce cas, la partie «graphique» de l'extension sera responsable de l'initialisation de l'affichage, et sa partie «physique» sera chargée de la mise en œuvre des lois physiques pour les corps rigides, par exemple la gravité. Les scènes contiennent des objets, donc une scène générique comprendra également plusieurs objets génériques. Les objets génériques peuvent également être étendus ou peuvent être étendus pour utiliser des propriétés graphiques, physiques et autres. Par exemple, le dessin d'un objet à l'écran sera implémenté par des fonctions d'expansion graphique, et le calcul de l'interaction des solides - par des fonctions physiques.

Un schéma détaillé de l'interaction entre le moteur et les systèmes est donné dans l'appendice B, "Schéma de l'interaction du moteur et des systèmes".
Il convient de noter que la scène générique et l'objet générique sont responsables de l'enregistrement de toutes leurs "extensions" auprès du gestionnaire d'état afin que toutes les extensions puissent être notifiées des modifications apportées par d'autres extensions (c'est-à-dire d'autres systèmes). Un exemple est une extension graphique enregistrée pour recevoir des notifications de changements de position et d'orientation effectués par extension physique.
Pour plus de détails sur les composants du système, reportez-vous à la Section 5.2, «Composants du système».

3.2. Gestionnaires

Les gestionnaires contrôlent le travail du moteur. Ce sont des objets Singleton, c'est-à-dire que chaque type de gestionnaire est disponible dans une seule instance. Cela est nécessaire car la duplication des ressources du gestionnaire entraînera inévitablement une redondance et un impact négatif sur les performances. En outre, les gestionnaires sont responsables de la mise en œuvre des fonctions communes pour tous les systèmes.
3.2.1. Gestionnaire des tâches
Le gestionnaire de tâches est responsable de la gestion des tâches système dans le pool de threads. Le pool de threads crée un thread pour chaque processeur afin d'assurer une mise à l'échelle optimale de n fois et pour éviter que des threads inutiles ne soient affectés, éliminant ainsi la surcharge de commutation de tâches inutiles dans le système d'exploitation.

Le planificateur fournit au gestionnaire de tâches une liste des tâches à effectuer, ainsi que des informations sur les tâches à attendre d'être terminées. Il reçoit ces données de divers systèmes. Chaque système n'a qu'une seule tâche à accomplir. Cette méthode est appelée décomposition fonctionnelle. Cependant, pour le traitement des données, chacune de ces tâches peut être divisée en un nombre arbitraire de sous-tâches (décomposition des données).
Voici un exemple de répartition des tâches entre les threads pour un système quadricœur.


Figure 6. Exemple de pool de threads utilisé par un gestionnaire de tâches

En plus de traiter les demandes d'accès aux tâches principales du planificateur, le gestionnaire de tâches peut travailler en mode d'initialisation. Il interroge séquentiellement les systèmes à partir de chaque thread afin qu'ils puissent initialiser les magasins de données locaux nécessaires au fonctionnement.
Des conseils pour la mise en œuvre d'un gestionnaire de tâches sont donnés dans l'annexe D, Conseils pour la mise en œuvre des tâches.

3.2.2. Gestionnaire d'état
Le gestionnaire d'état fait partie du moteur de messagerie. Il suit les modifications et envoie des notifications à leur sujet à tous les systèmes susceptibles d'être affectés par ces modifications. Afin de ne pas envoyer de notifications inutiles, le gestionnaire d'état stocke des informations sur les systèmes à notifier dans un cas particulier. Ce mécanisme est implémenté à l'aide du modèle Observer (voir l'annexe C, Observer (Design Pattern)). En bref, modèle donné implique l'utilisation d'un «observateur» qui surveille les changements sur le sujet, tandis que le rôle d'intermédiaire entre eux est joué par le contrôleur des changements.

Le mécanisme fonctionne comme suit. 1. L'observateur indique au contrôleur de modification (ou au gestionnaire d'état) les entités dont il souhaite suivre les modifications. 2. Le sujet informe le responsable du traitement de toutes ses modifications. 3. Sur un signal du cadre, le contrôleur informe l'observateur des changements dans le sujet. 4. L'observateur envoie une demande au sujet pour recevoir des données mises à jour.

En mode pas à pas libre (voir Section 2.1.1), l'implémentation de ce mécanisme est quelque peu compliquée. Tout d'abord, les données mises à jour devront être envoyées avec la notification de modification. Dans ce mode, l'envoi à la demande n'est pas applicable. En effet, si au moment de la réception de la demande, le système responsable des changements n'a pas encore fini de s'exécuter, il ne pourra pas fournir de données mises à jour. Deuxièmement, si un système n'est pas encore prêt à recevoir des modifications à la fin d'un cycle d'horloge, le gestionnaire d'état devra conserver les données modifiées jusqu'à ce que tous les systèmes enregistrés pour les recevoir soient prêts.

Le framework fournit deux gestionnaires d'état pour cela: pour le traitement des modifications au niveau de la scène et au niveau de l'objet. En règle générale, les messages sur les scènes et les objets sont indépendants les uns des autres, de sorte que l'utilisation de deux gestionnaires distincts élimine le besoin de traiter des données inutiles. Mais si la scène doit prendre en compte l'état d'un objet, vous pouvez l'enregistrer pour recevoir des notifications sur ses modifications.

Pour éviter une synchronisation inutile, le gestionnaire d'état génère une file d'attente de notification de modification séparément pour chaque thread créé par le gestionnaire de tâches. Par conséquent, aucune synchronisation n'est requise lors de l'accès à la file d'attente. La section 2.2 décrit une méthode qui peut être utilisée pour fusionner les files d'attente après l'exécution.


Figure 7. Notification des modifications internes d'un objet générique

Les notifications de modification n'ont pas besoin d'être envoyées de manière séquentielle. Il existe un moyen de les envoyer en parallèle. Lors de l'exécution d'une tâche, le système fonctionne avec tous ses objets. Par exemple, lorsque les objets physiques interagissent les uns avec les autres, le système physique contrôle leur mouvement, le calcul des collisions, de nouvelles forces agissantes, etc. Lors de la réception de notifications, l'objet système n'interagit pas avec les autres objets de son système. Il interagit avec ses extensions d'objet génériques associées. Cela signifie que les objets génériques sont désormais indépendants les uns des autres et peuvent être mis à jour en même temps. Cette approche n'exclut pas les cas extrêmes qui doivent être pris en compte lors du processus de synchronisation. Cependant, il permet l'utilisation du mode d'exécution parallèle quand il semble que vous ne pouvez agir que séquentiellement.

3.2.3. Gestionnaire de services
Le gestionnaire de services permet aux systèmes d'accéder à des fonctionnalités sur d'autres systèmes qui, autrement, ne leur seraient pas disponibles. Il est important de comprendre que les fonctions sont accessibles via des interfaces, pas directement. Les informations sur les interfaces système sont également stockées dans le gestionnaire de services.
Pour exclure la dépendance des systèmes les uns par rapport aux autres, chacun d'eux n'a petit ensemble prestations de service. En outre, la capacité d'utiliser un service particulier n'est pas déterminée par le système lui-même, mais par le gestionnaire de services.


Figure 8. Exemple de gestionnaire de services

Le responsable du service a également une autre fonction. Il permet aux systèmes d'accéder aux propriétés d'autres systèmes. Les propriétés sont des valeurs spécifiques au système qui ne sont pas communiquées dans le système de messagerie. Cela peut être une extension de la résolution de l'écran dans un système graphique ou l'amplitude de la gravité dans un système physique. Le gestionnaire de services permet aux systèmes d'accéder à ces données, mais ne les contrôle pas directement. Il place les modifications de propriété dans une file d'attente spéciale et ne les publie qu'après une exécution séquentielle. Veuillez noter que l'accès aux propriétés d'un autre système est rarement requis et ne doit pas être abusé. Par exemple, vous pourriez en avoir besoin pour activer ou désactiver le mode filaire dans le système graphique à partir de la fenêtre de la console, ou pour modifier la résolution de l'écran à la demande du lecteur à partir de l'interface utilisateur. Cette fonction est principalement utilisée pour définir des paramètres qui ne changent pas d'une image à l'autre.

3.2.4. Responsable environnement
  • Le gestionnaire d'environnement fournit l'environnement d'exécution du moteur. Ses fonctions peuvent être conditionnellement divisées dans les groupes suivants.
  • Variables: les noms et les valeurs des variables communes utilisées par toutes les parties du moteur. En règle générale, les valeurs des variables sont déterminées lorsque la scène est chargée ou certains paramètres utilisateur. Le moteur et divers systèmes peuvent y accéder en envoyant une requête.
  • Exécution: données d'exécution, telles que l'achèvement d'une scène ou d'un programme. Ces paramètres peuvent être définis et demandés à la fois par les systèmes eux-mêmes et par le moteur.
3.2.5. Gestionnaire de plateforme
Le gestionnaire de plateforme implémente une abstraction pour les appels du système d'exploitation et fournit également des fonctionnalités supplémentaires au-delà de la simple abstraction. L'avantage de cette approche est qu'elle encapsule plusieurs fonctions typiques dans un seul appel. Autrement dit, ils ne doivent pas être implémentés séparément pour chaque appelant, ce qui le surcharge avec des détails sur les appels du système d'exploitation.
Considérez, à titre d'exemple, appeler le gestionnaire de plateforme pour charger une bibliothèque dynamique système. Il charge non seulement le système, mais obtient également les points d'entrée de la fonction et appelle la fonction d'initialisation de la bibliothèque. Le gestionnaire stocke également le descripteur de bibliothèque et le décharge une fois le moteur terminé.

Le gestionnaire de plate-forme est également chargé de fournir des informations sur le processeur, telles que des instructions SIMD prises en charge, et d'initialiser un mode de fonctionnement spécifique des processus. Les systèmes ne peuvent pas utiliser d'autres fonctions pour générer des requêtes.

4. Interfaces

Les interfaces sont les moyens de communication entre le framework, les gestionnaires et les systèmes. Le cadre et les gestionnaires font partie du moteur, ils peuvent donc interagir directement les uns avec les autres. Les systèmes n'appartiennent pas au moteur. De plus, ils remplissent tous des fonctions différentes, ce qui conduit à la nécessité de créer une seule méthode d'interaction avec eux. Étant donné que les systèmes ne peuvent pas interagir directement avec les gestionnaires, ils doivent fournir un moyen d'accès différent. Cependant, toutes les fonctions des gestionnaires ne devraient pas être ouvertes aux systèmes. Certains d'entre eux ne sont disponibles que pour le framework.

Les interfaces définissent un ensemble de fonctions requises pour utiliser une méthode d'accès standard. Cela supprime la nécessité pour le framework de connaître les détails d'implémentation de systèmes spécifiques, car il ne peut interagir avec eux que via un ensemble spécifique d'appels.

4.1. Interfaces sujet et observateur

L'objectif principal des interfaces sujet et observateur est d'enregistrer les observateurs sur lesquels envoyer des notifications, ainsi que d'envoyer ces notifications. L'inscription et la déconnexion avec l'observateur sont des fonctions standard pour tous les sujets, incluses dans la mise en œuvre de leur interface.

4.2. Interfaces de gestionnaire

Les gestionnaires, bien qu'ils soient des objets Singleton, ne sont directement accessibles qu'au framework. D'autres systèmes ne peuvent accéder aux gestionnaires que via des interfaces qui ne représentent qu'une fraction de leurs fonctionnalités globales. Après l'initialisation, l'interface est transmise au système, qui l'utilise pour travailler avec certaines fonctions de gestionnaire.
Il n'y a pas d'interface unique pour tous les gestionnaires. Chacun d'eux a sa propre interface distincte.

4.3. Interfaces système

Pour qu'un framework puisse accéder aux composants du système, il a besoin d'interfaces. Sans eux, le soutien de tous nouveau système le moteur devrait être implémenté séparément.
Chaque système comprend quatre composants, il devrait donc y avoir quatre interfaces. À savoir: système, scène, objet et tâche. Description détaillée voir Section 5, Systèmes. Les interfaces sont les moyens d'accéder aux composants. Les interfaces système vous permettent de créer et de supprimer des scènes. Les interfaces de scène, à leur tour, vous permettent de créer et de détruire des objets, ainsi que de demander des informations sur la tâche principale du système. L'interface des tâches est principalement utilisée par le gestionnaire de tâches lors de la définition de tâches dans le pool de threads.
Puisque la scène et l'objet, en tant que partie du système, doivent interagir l'un avec l'autre et avec la scène universelle et l'objet auquel ils sont attachés, leurs interfaces sont également créées en fonction des interfaces du sujet et de l'observateur.

4.4. Changer les interfaces

Ces interfaces sont utilisées pour transférer des données entre les systèmes. Tous les systèmes effectuant des modifications d'un type particulier doivent implémenter une telle interface. La géométrie est un exemple. L'interface de géométrie comprend des procédés pour déterminer la position, l'orientation et l'échelle d'un élément. Tout système qui apporte des modifications à la géométrie doit implémenter une telle interface afin que les informations sur les autres systèmes ne soient pas nécessaires pour accéder aux données modifiées.

5. Systèmes

Les systèmes sont la partie du moteur qui est responsable de la mise en œuvre des fonctionnalités du jeu. Ils exécutent toutes les tâches de base sans lesquelles le moteur n'aurait pas de sens. L'interaction entre le moteur et les systèmes est réalisée à l'aide d'interfaces (voir Section 4.3, «Interfaces système»). Ceci est nécessaire pour ne pas surcharger le moteur avec des informations sur différents types systèmes. Les interfaces facilitent grandement l'ajout d'un nouveau système car le moteur n'a pas besoin de prendre en compte tous les détails de l'implémentation.

5.1. Les types

Les systèmes moteurs peuvent être grossièrement divisés en plusieurs catégories prédéfinies correspondant aux composants du jeu standard. Par exemple: géométrie, graphisme, physique (collision solide), son, traitement d'entrée, IA et animation.
Les systèmes dotés de fonctions non standard appartiennent à une catégorie distincte. Il est important de comprendre que tout système qui modifie des données pour une catégorie particulière doit être conscient de l'interface de cette catégorie, puisque le moteur ne fournit pas ces informations.

5.2. Composants du système

Plusieurs composants doivent être implémentés pour chaque système. Certains d'entre eux sont: système, scène, objet et tâche. Tous ces composants sont utilisés pour interagir avec différentes parties du moteur.
Le diagramme ci-dessous illustre les interactions entre les différents composants.


Figure 9. Composants du système

Un schéma détaillé des connexions entre les systèmes du moteur est donné à l'appendice B, «Schéma d'interaction entre le moteur et les systèmes».

5.2.1. Système
Le composant «système», ou simplement le système, est responsable de l'initialisation des ressources du système, qui ne changeront pratiquement pas pendant le fonctionnement du moteur. Par exemple, le système graphique analyse les adresses des ressources pour déterminer où elles se trouvent et pour accélérer le chargement lors de l'utilisation de la ressource. Il définit également la résolution de l'écran.
Le système est le principal point d'entrée du framework. Il fournit des informations sur lui-même (par exemple, le type de système), ainsi que des méthodes de création et de suppression de scènes.
5.2.2. Scène
Le composant de scène, ou scène système, est responsable de la gestion des ressources associées à la scène actuelle. La scène générique utilise des scènes système pour étendre les fonctionnalités en tirant parti de leurs fonctionnalités. Un exemple est une scène physique qui est utilisée pour créer un nouveau monde de jeu et, lors de l'initialisation de la scène, détermine les forces de gravité qu'elle contient.
Les scènes fournissent des méthodes pour créer et détruire des objets, ainsi qu'un composant "tâche" pour traiter la scène et une méthode pour y accéder.
5.2.3. Un objet
Le composant objet, ou objet système, appartient à la scène et est généralement associé à ce que l'utilisateur voit à l'écran. Un objet générique utilise un objet système pour étendre les fonctionnalités en exposant ses propriétés comme les siennes.
Un exemple serait l'extension géométrique, graphique et physique d'un objet générique pour afficher une poutre en bois sur un écran. Les propriétés géométriques comprendront la position, l'orientation et l'échelle de l'objet. Le système graphique utilisera une grille spéciale pour l'afficher. Et le système physique le dotera des propriétés d'un corps rigide pour le calcul des interactions avec d'autres corps et des forces de gravité agissant.

Dans certains cas, un objet système doit prendre en compte les modifications apportées à un objet générique ou à l'une de ses extensions. À cette fin, vous pouvez créer une relation spéciale qui suivra les modifications apportées.

5.2.4. Une tâche
Le composant de tâche, ou tâche système, est utilisé pour traiter une scène. La tâche reçoit une commande de mise à jour de la scène du gestionnaire de tâches. Il s'agit d'un signal pour exécuter des fonctions système sur des objets de scène.
L'exécution d'une tâche peut être divisée en sous-tâches, en les distribuant également à l'aide du gestionnaire de tâches à un nombre encore plus grand de threads. C'est un moyen pratique de faire évoluer le moteur sur plusieurs processeurs. Cette technique s'appelle la décomposition des données.
Les informations sur les changements d'objet dans le processus de mise à jour des tâches de scène sont transmises au gestionnaire d'état. Pour plus de détails sur le gestionnaire d'état, reportez-vous à la section 3.2.2.

6. Combinaison de tous les composants

Tous les éléments décrits ci-dessus sont liés les uns aux autres et font partie d'un tout. Le travail du moteur peut être grossièrement divisé en plusieurs étapes, décrites dans les sections suivantes.

6.1. Phase d'initialisation

Le moteur démarre avec l'initialisation des gestionnaires et du framework.
  • Le framework appelle le chargeur de scène.
  • Après avoir déterminé quels systèmes la scène utilisera, le chargeur appelle le gestionnaire de plateforme pour charger les modules appropriés.
  • Le gestionnaire de plateforme charge les modules appropriés et les transmet au gestionnaire d'interface, puis les appelle pour créer un nouveau système.
  • Le module renvoie au chargeur un pointeur vers une instance du système qui implémente l'interface système.
  • Le gestionnaire de services enregistre tous les services fournis par le module système.


Figure 10. Initialisation des gestionnaires et des systèmes moteurs

6.2. Étape de chargement de la scène

Le contrôle est renvoyé au chargeur, qui charge la scène.
  • Le chargeur crée une scène générique. Pour instancier des scènes système, il appelle les interfaces système, étendant les fonctionnalités de la scène générique.
  • La scène universelle définit les données que chaque scène système peut modifier et les notifications sur les modifications qu'elle doit recevoir.
  • En faisant correspondre les scènes qui apportent certaines modifications et qui souhaitent en être notifiées, la scène générique transmet ces informations au gestionnaire d'état.
  • Pour chaque objet de la scène, le chargeur crée un objet générique, puis détermine quels systèmes étendront l'objet générique. La correspondance entre les objets système est déterminée selon le même schéma que celui utilisé pour les scènes. Il est également transmis au gestionnaire d'état.
  • Le chargeur utilise les interfaces de scène résultantes pour instancier des objets système et les utiliser pour étendre des objets génériques.
  • Le planificateur interroge les interfaces de scène pour leurs tâches principales afin qu'elles puissent transmettre ces informations au gestionnaire de tâches pendant l'exécution.


Figure 11. Initialisation de la scène universelle et de l'objet

6.3. Étape du cycle de jeu

  • Le gestionnaire de plate-forme est utilisé pour gérer les messages de fenêtre et d'autres éléments nécessaires au fonctionnement de la plate-forme actuelle.
  • Le contrôle passe ensuite au planificateur, qui attend la fin de l'horloge pour continuer à fonctionner.
  • À la fin de l'horloge, dans un mode pas à pas libre, le planificateur vérifie quelles tâches ont été effectuées. Toutes les tâches terminées (c'est-à-dire prêtes à être exécutées) sont transférées au gestionnaire de tâches.
  • Le planificateur détermine quelles tâches seront terminées dans la graduation actuelle et attend qu'elles soient terminées.
  • En mode pas à pas dur, ces opérations sont répétées à chaque mesure. Le planificateur soumet toutes les tâches au gestionnaire et attend qu'elles soient terminées.
6.3.1. Terminer la tâche
Le contrôle est transféré au gestionnaire de tâches.
  • Il forme une file d'attente de toutes les tâches reçues, puis, lorsque des threads libres apparaissent, commence leur exécution. (Le processus d'exécution des tâches diffère selon les systèmes. Les systèmes peuvent travailler avec une seule tâche ou traiter plusieurs tâches de la file d'attente en même temps, réalisant ainsi une exécution parallèle.)
  • Au cours de l'exécution, les tâches peuvent fonctionner avec la scène entière ou uniquement avec certains objets, en modifiant leurs données internes.
  • Les systèmes doivent être informés de toute modification des données générales (par exemple, position ou orientation). Par conséquent, lors de l'exécution d'une tâche, la scène ou l'objet système informe l'observateur de tout changement. Dans ce cas, l'observateur agit en fait comme un contrôleur de changement, qui fait partie du gestionnaire d'état.
  • Le contrôleur de modification génère une file d'attente de notifications de modification pour un traitement ultérieur. Il ignore les changements qui n'affectent pas l'observateur donné.
  • Pour utiliser des services spécifiques, une tâche contacte le gestionnaire de services. Le gestionnaire de services vous permet également de modifier les propriétés d'autres systèmes qui ne sont pas disponibles pour la transmission dans le moteur de messagerie (par exemple, le système de saisie de données modifie l'extension d'écran - une propriété du système graphique).
  • Les tâches peuvent également contacter le gestionnaire d'environnement pour obtenir des variables d'environnement et modifier l'état d'exécution (suspendre l'exécution, passer à la scène suivante, etc.).


Figure 12. Gestionnaire de tâches et tâches

6.3.2. Mise à jour des données
Après avoir terminé toutes les tâches du tick actuel, la boucle de jeu principale appelle le gestionnaire d'état pour démarrer la phase de mise à jour des données.
  • Le gestionnaire d'état appelle chacun de ses contrôleurs de changement à son tour pour envoyer les notifications accumulées. Le responsable du traitement vérifie à quels observateurs envoyer des notifications de modification pour chaque sujet.
  • Il appelle ensuite l'observateur souhaité et l'informe du changement (la notification comprend également un pointeur vers l'interface objet). En mode pas à pas libre, l'observateur reçoit les données modifiées du contrôleur de changement, mais en mode pas à pas dur, il doit les demander au sujet lui-même.
  • En règle générale, les observateurs intéressés par la réception de notifications de modification d'objet système sont d'autres objets système associés au même objet générique. Cela vous permet de diviser le processus de modification en plusieurs tâches qui peuvent être effectuées en parallèle. Pour simplifier le processus de synchronisation, vous pouvez combiner toutes les extensions d'objet générique associées en une seule tâche.
6.3.3. Vérifier la progression et quitter
La dernière étape de la boucle de jeu consiste à vérifier l'état du runtime. Il existe plusieurs états de ce type: exécution, pause, scène suivante, etc. Si l'état "courant" est sélectionné, la prochaine itération de la boucle démarrera. L'état de sortie signifie la fin de la boucle, la libération des ressources et la sortie de l'application. D'autres états peuvent être implémentés, par exemple "pause", "scène suivante", etc.

7. Conclusion

L'idée principale de cet article est exposée dans la section 2, «État d'exécution parallèle». Grâce à la décomposition fonctionnelle et à la décomposition des données, il est possible de mettre en œuvre non seulement le multithreading du moteur, mais également son évolutivité à un nombre encore plus grand de cœurs dans le futur. Pour éliminer la surcharge de synchronisation tout en gardant vos données à jour, utilisez des gestionnaires d'état en plus de la messagerie.

Le modèle Observer est une fonction du moteur de messagerie. Il est important de bien comprendre son fonctionnement afin de choisir la meilleure façon de l'implémenter pour le moteur. En fait, il s'agit d'un mécanisme d'interaction entre différents systèmes, qui assure la synchronisation des données communes.

La gestion des tâches joue un rôle important dans l'équilibrage de charge. L'annexe D fournit des conseils pour créer un gestionnaire de tâches efficace pour un moteur de jeu.

Comme vous pouvez le voir, le multithreading du moteur de jeu est possible grâce à une structure et un mécanisme de messagerie bien définis. Il peut considérablement améliorer les performances des processeurs actuels et futurs.

Annexe A. Schéma du moteur

Le traitement est lancé à partir de la boucle de jeu principale (voir Fig. 4, «Boucle de jeu principale»).


Appendice B. Schéma d'interaction entre le moteur et les systèmes


Annexe C.Observateur (modèle de conception)

Le modèle Observer est décrit en détail dans le livre Techniques de conception orientée objet. Modèles de conception ", E. Gamma, R. Helm, R. Johnson, J. Vlissides (" Modèles de conception: éléments de logiciel orienté objet réutilisable ", Gamma E., Helm R., Johnson R., Vlissides J.). Il a été publié pour la première fois en anglais en 1995 par Addison-Wesley.

L'idée principale de ce modèle est la suivante: si certains éléments doivent être notifiés des modifications apportées à d'autres éléments, ils n'ont pas à parcourir la liste de tous les changements possibles, en essayant d'y trouver les données nécessaires. Le modèle suppose l'existence d'un acteur et d'un observateur, qui sont utilisés pour envoyer des notifications de modifications. L'observateur surveille toute modification du sujet. Le contrôleur de changement agit comme intermédiaire entre ces deux composants. Le diagramme suivant illustre cette relation.


Figure 13. Modèle d'observateur

Le processus d'utilisation de ce modèle est décrit ci-dessous.

  1. Le contrôleur des modifications enregistre l'observateur et le sujet pour lesquels il souhaite être notifié.
  2. Le contrôleur de changement est en fait un observateur. Au lieu de l'observateur, il s'enregistre avec le sujet. Le contrôleur des modifications tient également à jour sa propre liste d'observateurs et de sujets enregistrés auprès d'eux.
  3. Une entité ajoute un observateur (c'est-à-dire le contrôleur des modifications) à sa liste d'observateurs qui souhaitent être informés de ses modifications. Parfois, le type de changement est en plus indiqué, ce qui détermine les changements qui intéressent l'observateur. Cela vous permet de rationaliser le processus d'envoi des notifications de modification.
  4. Lors du changement de données ou d'état, le sujet en informe l'observateur via un mécanisme de rappel et communique des informations sur les types modifiés.
  5. Le contrôleur de modification forme une file d'attente de notifications sur les modifications et attend un signal pour les distribuer aux objets et aux systèmes.
  6. Pendant la distribution, le contrôleur de changement s'adresse à de vrais observateurs.
  7. Les observateurs demandent des informations sur les données ou l'état modifiés du sujet (ou les reçoivent avec les notifications).
  8. Avant de supprimer un observateur, ou s'il n'a plus besoin d'être notifié d'un sujet, il supprime l'abonnement à ce sujet dans le contrôleur de modification.
Il y a beaucoup de différentes façons mettre en œuvre la distribution des tâches. Cependant, il est préférable de maintenir le nombre de threads de travail égal au nombre de processeurs logiques de plate-forme disponibles. Essayez de ne pas lier les tâches à un fil spécifique. Le temps d'exécution des tâches de différents systèmes ne coïncide pas toujours. Cela peut entraîner une répartition inégale des charges de travail entre les threads de travail et affecter l'efficacité. Pour faciliter ce processus, utilisez des bibliothèques de gestion des tâches telles que

Après avoir traité de la théorie du multithreading, considérons un exemple pratique - le Pentium 4. Déjà au stade de développement de ce processeur, les ingénieurs d'Intel ont continué à travailler sur l'augmentation de ses performances sans introduire de changements dans l'interface du programme. Cinq moyens les plus simples ont été envisagés:

Augmenter la fréquence d'horloge;

Placement de deux processeurs sur un microcircuit;

Introduction de nouveaux blocs fonctionnels;

Extension du convoyeur;

Utilisation du multithreading.

La manière la plus évidente d'améliorer les performances consiste à augmenter la vitesse d'horloge sans modifier les autres paramètres. En règle générale, chaque modèle de processeur suivant a une vitesse d'horloge légèrement plus élevée que le précédent. Malheureusement, avec une simple augmentation de la fréquence d'horloge, les développeurs sont confrontés à deux problèmes: une augmentation de la consommation d'énergie (ce qui est important pour les ordinateurs portables et autres appareils informatiques fonctionnant sur batteries) et la surchauffe (qui nécessite des dissipateurs de chaleur plus efficaces).

La deuxième méthode - placer deux processeurs sur un microcircuit - est relativement simple, mais elle consiste à doubler la surface occupée par le microcircuit. Si chaque processeur est fourni avec sa propre mémoire cache, le nombre de puces sur un plateau est divisé par deux, mais cela signifie également un doublement des coûts de production. En fournissant un cache partagé pour les deux processeurs, une augmentation significative de l'encombrement peut être évitée, mais dans ce cas, un autre problème se pose: la quantité de mémoire cache par processeur est divisée par deux, ce qui affecte inévitablement les performances. En outre, alors que les applications serveur professionnelles sont capables d'utiliser pleinement les ressources de plusieurs processeurs, dans les programmes de bureau ordinaires, le parallélisme interne est beaucoup moins développé.

L'introduction de nouveaux blocs fonctionnels n'est pas non plus difficile, mais il est important de trouver un équilibre ici. Quel est l'intérêt d'une douzaine de blocs ALU si le microcircuit ne peut pas envoyer de commandes au convoyeur à une vitesse telle qu'il puisse charger tous ces blocs?

Un convoyeur avec un nombre accru d'échelons, capable de diviser les tâches en segments plus petits et de les traiter en peu de temps, d'une part, augmente la productivité, d'autre part, augmente les conséquences négatives des transitions mal prévues, des échecs de cache, des interruptions et d'autres événements qui perturbent le flux normal traitement des commandes dans le processeur. De plus, pour réaliser pleinement les capacités du pipeline étendu, il est nécessaire d'augmenter la fréquence d'horloge, ce qui, comme nous le savons, entraîne une augmentation de la consommation d'énergie et de la dissipation thermique.

Enfin, vous pouvez implémenter le multithreading. L'avantage de cette technologie est qu'elle introduit un thread logiciel supplémentaire pour activer des ressources matérielles qui seraient autrement inactives. Sur la base des résultats d'études expérimentales, les développeurs Intel ont constaté qu'une augmentation de 5% de la surface de la puce lors de la mise en œuvre du multithreading pour de nombreuses applications se traduit par un gain de performances de 25%. Le premier processeur Intel à prendre en charge le multithreading était le 2002 Heon. Par la suite, à partir de 3,06 GHz, le multithreading a été introduit dans la ligne Pentium 4. Intel appelle l'implémentation du multithreading dans l'hyperthreading Pentium 4.

  • Didacticiel

Dans cet article, je vais essayer de décrire la terminologie utilisée pour décrire les systèmes capables d'exécuter plusieurs programmes en parallèle, c'est-à-dire multicœur, multiprocesseur, multithread. Les différents types de parallélisme dans les processeurs IA-32 sont apparus à des moments différents et dans un ordre quelque peu incohérent. Il est assez facile de se perdre dans tout cela, d'autant plus que les systèmes d'exploitation cachent soigneusement les détails des applications pas trop sophistiquées.

Le but de l'article est de montrer qu'avec toute la variété de configurations possibles de systèmes multiprocesseurs, multicœurs et multithread pour les programmes qui s'exécutent sur eux, des opportunités sont créées à la fois pour l'abstraction (en ignorant les différences) et pour la prise en compte des spécificités (la possibilité de trouver la configuration par programmation).

Mark warning ®, ™, dans l'article

Le mien explique pourquoi les employés de l'entreprise devraient utiliser des marques de droit d'auteur dans les communications publiques. Dans cet article, j'ai dû les utiliser assez souvent.

CPU

Bien entendu, le terme le plus ancien, le plus souvent utilisé et controversé est "processeur".

Dans le monde moderne, un processeur est ce que nous achetons dans une belle boîte de vente au détail ou dans un emballage OEM pas si beau. Une entité indivisible insérée dans une socket sur carte mère... Même s'il n'y a pas de connecteur et ne peut pas être retiré, c'est-à-dire s'il est fermement soudé, il s'agit d'une seule puce.

Les systèmes mobiles (téléphones, tablettes, ordinateurs portables) et la plupart des ordinateurs de bureau ont un seul processeur. Les postes de travail et les serveurs disposent parfois de deux processeurs ou plus sur une seule carte mère.

La prise en charge de plusieurs processeurs dans un système nécessite de nombreuses modifications de conception. Au minimum, il faut s'assurer de leur connexion physique (prévoir plusieurs sockets sur la carte mère), résoudre les problèmes d'identification du processeur (voir plus loin dans cet article, ainsi que ma note), négocier les accès mémoire et délivrer des interruptions (le contrôleur d'interruption doit être capable d'acheminer les interruptions vers plusieurs processeurs) et, bien sûr, le support du système d'exploitation. Malheureusement, je n'ai pas trouvé de mention documentaire de la création du premier système multiprocesseur sur les processeurs Intel, cependant, Wikipédia affirme que Sequent Computer Systems les a déjà fournis en 1987, en utilisant des processeurs Intel 80386. Un support étendu pour plusieurs puces dans un système devient disponible en commençant par Intel® Pentium.

S'il y a plusieurs processeurs, chacun d'eux a son propre connecteur sur la carte. En même temps, chacun d'eux dispose de copies indépendantes complètes de toutes les ressources, telles que les registres, les exécuteurs, les caches. Ils partagent une mémoire commune - RAM. La mémoire peut être connectée à eux de différentes manières et plutôt non triviales, mais c'est une histoire distincte qui sort du cadre de cet article. Il est important que dans n'importe quel scénario, les programmes exécutables doivent créer l'illusion d'une mémoire partagée homogène disponible à partir de tous les processeurs inclus dans le système.


Prêt à décoller! Carte mère Intel® D5400XS

Noyau

Historiquement, le multicœur dans Intel IA-32 est apparu plus tard que Intel® HyperThreading, mais dans la hiérarchie logique, il vient ensuite.

Il semblerait que si le système a plus de processeurs, ses performances sont plus élevées (sur les tâches qui peuvent utiliser toutes les ressources). Cependant, si le coût de la communication entre eux est trop élevé, alors tout le gain du parallélisme est tué par de longs retards dans le transfert des données partagées. C'est exactement ce que l'on observe dans les systèmes multiprocesseurs - à la fois physiquement et logiquement, ils sont très éloignés les uns des autres. Pour communiquer efficacement dans un tel environnement, des bus spécialisés tels que Intel® QuickPath Interconnect doivent être inventés. La consommation d'énergie, la taille et le prix de la solution finale, bien sûr, ne diminuent pas de tout cela. L'intégration élevée des composants devrait venir à la rescousse - les circuits exécutant des parties du programme parallèle devraient être rapprochés les uns des autres, de préférence sur un cristal. En d'autres termes, un processeur doit organiser plusieurs noyaux, tous identiques les uns aux autres, mais travaillant indépendamment.

Les premiers processeurs IA-32 multicœurs d'Intel ont été introduits en 2005. Depuis lors, le nombre moyen de cœurs dans les plates-formes de serveurs, de bureau et maintenant mobiles n'a cessé de croître.

Contrairement à deux processeurs monocœur sur le même système, partageant uniquement la mémoire, deux cœurs peuvent également partager des caches et d'autres ressources responsables de l'interaction avec la mémoire. Le plus souvent, les caches de premier niveau restent privés (chaque noyau a le sien), tandis que les deuxième et troisième niveaux peuvent être partagés ou séparés. Cette organisation du système permet de réduire les délais de livraison des données entre les cœurs voisins, surtout s'ils travaillent sur une tâche commune.


Une micrographie d'un processeur Intel quad-core, nom de code Nehalem. Des cœurs séparés, un cache L3 partagé, des liens QPI vers d'autres processeurs et un contrôleur de mémoire commun sont alloués.

Hyper-Threading

Jusqu'en 2002 environ, la seule façon d'obtenir un système IA-32 capable d'exécuter deux programmes ou plus en parallèle était d'utiliser des systèmes multiprocesseurs. L'Intel® Pentium® 4, ainsi que la ligne Xeon, baptisée Foster (Netburst), ont introduit une nouvelle technologie - l'hyperthreading ou l'hyperthreading - Intel® HyperThreading (ci-après HT).

Il n'y a rien de nouveau sous le soleil. HT est un cas particulier de ce que la littérature appelle le multithreading simultané (SMT). Contrairement aux "vrais" cœurs, qui sont des copies complètes et indépendantes, dans le cas de HT, seule une partie des nœuds internes est dupliquée dans un seul processeur, principalement responsable du stockage des registres d'état de l'architecture. Les nœuds exécutifs chargés de l'organisation et du traitement des données restent au singulier, et à tout moment sont utilisés par au plus l'un des threads. Comme les noyaux, les hyperthreads partagent des caches entre eux, mais à partir de quel niveau cela dépend du système spécifique.

Je n'essaierai pas d'expliquer tous les avantages et inconvénients des conceptions avec SMT en général et avec HT en particulier. Le lecteur intéressé peut trouver une discussion assez détaillée de la technologie dans de nombreuses sources, et bien sûr sur Wikipedia. Cependant, je noterai ce qui suit point importantexpliquer les limites actuelles du nombre d'hyper-threads dans les produits réels.

Limites de flux
Quand la présence de multicœur «malhonnête» sous forme de HT est-elle justifiée? Si un thread d'application est incapable de charger tous les nœuds en cours d'exécution à l'intérieur du noyau, alors ils peuvent être "empruntés" à un autre thread. Ceci est typique pour les applications qui ont un goulot d'étranglement non pas dans les calculs, mais dans l'accès aux données, c'est-à-dire qu'elles génèrent souvent des erreurs de cache et doivent attendre que les données soient livrées à partir de la mémoire. À ce stade, le noyau sans HT sera forcé de rester inactif. La présence de HT vous permet de basculer rapidement les nœuds d'exécution libres vers un autre état architectural (car il est simplement dupliqué) et d'exécuter ses instructions. Il s'agit d'un cas particulier d'une technique appelée masquage de latence, où une opération longue, au cours de laquelle des ressources utiles sont inactives, est masquée par l'exécution parallèle d'autres tâches. Si l'application utilise déjà beaucoup les ressources du noyau, la présence d'hyperthreads ne lui permettra pas de s'accélérer - des noyaux «honnêtes» sont nécessaires ici.

Scénarios typiques pour les applications de bureau et serveur conçues pour les architectures de machines usage généralont le potentiel de parallélisme mis en œuvre par HT. Cependant, ce potentiel est rapidement «épuisé». Peut-être pour cette raison, sur presque tous les processeurs IA-32, le nombre d'hyperthreads matériels ne dépasse pas deux. Dans des scénarios typiques, le gain lié à l'utilisation de trois hyper-threads ou plus serait faible, mais la perte de taille du cristal, de sa consommation d'énergie et de son coût est importante.

Une situation différente est observée dans les tâches typiques effectuées sur les accélérateurs vidéo. Par conséquent, ces architectures se caractérisent par l'utilisation de techniques SMT avec un grand nombre de threads. Étant donné que les coprocesseurs Intel® Xeon Phi (introduits en 2010) sont idéologiquement et généalogiquement assez proches des cartes vidéo, ils peuvent être quatre hyperthreading sur chaque cœur - une configuration unique à IA-32.

Processeur logique

Des trois "niveaux" de parallélisme décrits (processeurs, cœurs, hyperthreads), certains d'entre eux ou tous peuvent manquer dans un système particulier. Ceci est influencé par paramètres du BIOS (le multicœur et le multithreading sont désactivés indépendamment), les fonctionnalités microarchitecturales (par exemple, HT manquait dans Intel® Core ™ Duo, mais a été renvoyé avec la sortie de Nehalem) et les événements système (les serveurs multiprocesseurs peuvent arrêter les processeurs défaillants en cas de panne et continuer à «voler» sur le reste). Comment ce zoo multiniveau de parallélisme est-il visible pour le système d'exploitation et, finalement, pour l'application?

De plus, pour plus de commodité, nous désignons le nombre de processeurs, cœurs et threads dans un certain système par le triple ( x, y, z), où x est le nombre de processeurs y est le nombre de cœurs dans chaque processeur, et z - le nombre d'hyperthreads dans chaque cœur. Dans ce qui suit, j'appellerai ce triple topologie - un terme établi qui n'a pas grand-chose à voir avec la section des mathématiques. Composition p = xyz définit le nombre d'entités nommées processeurs logiques systèmes. Il définit le nombre total de contextes indépendants de processus d'application dans un système de mémoire partagée fonctionnant en parallèle que le système d'exploitation est obligé de prendre en compte. Je dis «forcé» car il ne peut pas contrôler l'ordre d'exécution de deux processus sur des processeurs logiques différents. Ceci s'applique également aux hyper-threads: bien qu'ils fonctionnent "séquentiellement" sur le même cœur, l'ordre spécifique est dicté par le matériel et n'est pas disponible pour surveiller ou contrôler les programmes.

Le plus souvent, le système d'exploitation masque aux applications finales les caractéristiques de la topologie physique du système sur lequel il s'exécute. Par exemple, les trois topologies suivantes: (2, 1, 1), (1, 2, 1) et (1, 1, 2) - le système d'exploitation sera représenté comme deux processeurs logiques, bien que le premier d'entre eux ait deux processeurs, le second - deux cœurs, et le troisième n'a que deux threads.


Le Gestionnaire des tâches de Windows montre 8 processeurs logiques; mais combien coûte-t-il dans les processeurs, les cœurs et les hyperthreads?


Le dessus de Linux montre 4 processeurs logiques.

C'est très pratique pour les développeurs d'applications - ils n'ont pas à gérer les fonctionnalités matérielles qui ne leur sont souvent pas pertinentes.

Définition de la topologie par programmation

Bien sûr, l'abstraction de la topologie en un seul nombre de processeurs logiques crée dans certains cas des motifs suffisants pour la confusion et les malentendus (dans les conflits Internet houleux). Les applications informatiques qui veulent tirer le maximum de performances du fer nécessitent un contrôle détaillé de l'emplacement de leurs threads: plus près les uns des autres sur les hyper-threads voisins, ou, au contraire, plus loin sur différents processeurs. La vitesse de communication entre les processeurs logiques dans un seul cœur ou processeur est beaucoup plus élevée que la vitesse de transfert de données entre processeurs. Possibilité d'hétérogénéité dans l'organisation mémoire vive complique également le tableau.

Les informations sur la topologie du système dans son ensemble, ainsi que la position de chaque processeur logique dans IA-32 sont disponibles à l'aide de l'instruction CPUID. Depuis l'apparition des premiers systèmes multiprocesseurs, le schéma d'identification du processeur logique a été étendu plusieurs fois. À ce jour, certaines parties de celui-ci sont contenues dans les feuilles 1, 4 et 11 du CPUID. La feuille à regarder peut être déterminée à partir de l'organigramme suivant extrait de l'article:

Je ne vais pas m'ennuyer ici avec tous les détails des différentes parties de cet algorithme. En cas d'intérêt, la partie suivante de cet article peut y être consacrée. Je renvoie le lecteur intéressé à, dans lequel cette question est examinée avec le plus de détails possible. Ici, je vais d'abord décrire brièvement ce qu'est l'APIC et comment il se rapporte à la topologie. Pensez alors à travailler avec la feuille 0xB (onze en décimal), qui est actuellement le dernier mot de "apicostroenie".

ID APIC
L'APIC local (contrôleur d'interruption programmable avancé) est un appareil (qui fait maintenant partie du processeur) chargé de travailler avec des interruptions venant d'un processeur logique spécifique. Chaque processeur logique possède son propre APIC. Et chacun d'eux dans le système doit avoir valeur unique ID APIC. Ce numéro est utilisé par les contrôleurs d'interruption pour l'adressage lors de la remise des messages, et par tout le monde (par exemple, le système d'exploitation) pour identifier les processeurs logiques. La spécification de ce contrôleur d'interruption a évolué de l'Intel 8259 PIC en passant par Dual PIC, APIC et xAPIC à x2APIC.

Actuellement, la largeur du nombre stocké dans l'ID APIC a atteint les 32 bits complets, bien que dans le passé elle était limitée à 16, et même plus tôt - seulement 8 bits. De nos jours, les restes de l'ancien temps sont dispersés dans tout le CPUID, mais les 32 bits de l'ID APIC sont retournés à CPUID.0xB.EDX. Chaque processeur logique, exécutant indépendamment l'instruction CPUID, renverra sa propre valeur.

Clarification des liens familiaux
La valeur ID APIC seule ne dit rien sur la topologie. Pour savoir quels sont les deux processeurs logiques à l'intérieur d'un processeur physique (c'est-à-dire qu'ils sont «frères» d'hyperthreads), lesquels sont à l'intérieur du même processeur et lesquels sont dans des processeurs complètement différents, vous devez comparer leurs valeurs d'identification APIC. Selon le degré de relation, certains de leurs bits seront les mêmes. Ces informations sont contenues dans les sous-listes CPUID.0xB, qui sont codées à l'aide de l'opérande ECX. Chacun d'eux décrit la position du champ de bits de l'un des niveaux de topologie dans EAX (plus précisément, le nombre de bits qui doivent être décalés dans l'ID APIC vers la droite pour supprimer les niveaux de topologie inférieurs), ainsi que le type de ce niveau - hyperthread, core ou processeur - dans ECX.

Les processeurs logiques situés à l'intérieur du même cœur auront les mêmes bits d'identification APIC, à l'exception de ceux appartenant au champ SMT. Pour les processeurs logiques dans le même processeur, tous les bits à l'exception des champs Core et SMT. Étant donné que le nombre de sous-listes pour CPUID.0xB peut augmenter, ce schéma permettra à la description des topologies d'être prise en charge avec plus de niveaux, si le besoin s'en fait sentir à l'avenir. De plus, il sera possible d'entrer des niveaux intermédiaires entre les niveaux existants.

Une conséquence importante de l'organisation de ce schéma est qu'il peut y avoir des «trous» dans l'ensemble de tous les ID APIC de tous les processeurs logiques du système; ils n'iront pas séquentiellement. Par exemple, dans un processeur multicœur avec HT désactivé, tous les ID APIC peuvent être pairs, car le bit le moins significatif responsable du codage du numéro d'hyperstream sera toujours zéro.

Notez que CPUID.0xB n'est pas la seule source de informations sur les processeurs logiques disponibles pour le système d'exploitation. Une liste de tous les processeurs disponibles, ainsi que leurs valeurs d'ID APIC, est codée dans la table MADT ACPI.

Systèmes d'exploitation et topologie

Les systèmes d'exploitation fournissent des informations de topologie de processeur logique aux applications via leurs propres interfaces.

Sous Linux, les informations de topologie sont contenues dans le pseudo fichier / proc / cpuinfo et la sortie dmidecode. Dans l'exemple ci-dessous, je filtre le contenu cpuinfo sur un système quad core sans HT, ne laissant que les entrées liées à la topologie:

Texte masqué

[email protected]: ~ $ cat / proc / cpuinfo | grep "processor \\ | physical \\ id \\ | siblings \\ | core \\ | cores \\ | apicid" processor: 0 physique id: 0 frères et sœurs: 4 core id: 0 cpu core: 2 apicid: 0 apicid initial: 0 processeur: 1 identifiant physique: 0 frères et sœurs: 4 id de cœur: 0 cœurs de processeur: 2 apicid: 1 apicid initial: 1 processeur: 2 id physique: 0 frères et sœurs: 4 id de cœur: 1 cœurs de processeur: 2 apicid: 2 apicid initial: 2 processeur: 3 id physique: 0 frères et sœurs: 4 id de cœur: 1 cœurs de processeur: 2 apicid: 3 apicid initial: 3

Dans FreeBSD, la topologie est signalée via le mécanisme sysctl dans la variable kern.sched.topology_spec au format XML:

Texte masqué

[email protected]: ~ $ sysctl kern.sched.topology_spec kern.sched.topology_spec: 0, 1, 2, 3, 4, 5, 6, 7 0, 1, 2, 3, 4, 5, 6, 7 0, 1 Groupe THREADGroupe SMT 2, 3 Groupe THREADGroupe SMT 4, 5 Groupe THREADGroupe SMT 6, 7 Groupe THREADGroupe SMT

Dans MS Windows 8, les informations de topologie peuvent être consultées dans le Gestionnaire des tâches.

Pour industrie de l'information le début du 21e siècle a coïncidé avec des changements que l'on peut qualifier de «tectoniques». Les signes d'une nouvelle ère incluent l'utilisation d'architectures orientées services (SOA), de configurations de cluster et bien plus encore, y compris des processeurs multicœurs. Mais, bien sûr, la raison fondamentale de ce qui se passe est le développement de la physique des semi-conducteurs, qui a abouti à une augmentation du nombre d'éléments logiques par unité de surface, obéissant à la loi de Gordon Moore. Le nombre de transistors sur une puce se chiffre déjà à des centaines de millions et dépassera bientôt la barre du milliard de dollars, à la suite de quoi l'action de la loi bien connue de la dialectique, qui postule la relation entre les changements quantitatifs et qualitatifs, se manifeste inévitablement. Dans les conditions modifiées, une nouvelle catégorie apparaît - complexité, et les systèmes deviennent complexes à la fois au niveau micro (processeurs) et au niveau macro (systèmes d'information d'entreprise).

Dans une certaine mesure, ce qui se passe dans le monde informatique moderne peut être comparé à la transition évolutive qui a eu lieu il y a des millions d'années, lorsque les organismes multicellulaires sont apparus. À ce moment-là, la complexité d'une cellule avait atteint une certaine limite, et l'évolution ultérieure a suivi la voie du développement de la complexité de l'infrastructure. La même chose se produit avec systèmes informatiques: la complexité d'un cœur de processeur unique, ainsi que de l'architecture monolithique d'entreprise systèmes d'information atteint un certain maximum. Désormais, au niveau macro, il y a une transition des systèmes monolithiques vers des composants (ou composés de services), et l'attention des développeurs se concentre sur les logiciels d'infrastructure de la couche intermédiaire, et au niveau micro, de nouvelles architectures de processeurs apparaissent.

Littéralement très récemment, le concept de complexité a commencé à perdre son sens communément utilisé pour devenir un facteur indépendant. À cet égard, la complexité n'est pas encore pleinement comprise et l'attitude à son égard n'est pas complètement définie, bien que, assez curieusement, il existe depuis près de 50 ans une discipline scientifique distincte, appelée «théorie des systèmes complexes». (Rappelons qu'en théorie, «complexe» est un système dont les composants individuels sont combinés de manière non linéaire; un tel système n'est pas simplement une somme de composants, comme c'est le cas dans les systèmes linéaires.) On ne peut que s'étonner que la théorie des systèmes n'a pas encore été acceptée par ces spécialistes et entreprises, dont l'activité les mène à la création de ces systèmes complexes au moyen des technologies de l'information.

"Gorge de bouteille" de l'architecture von Neumann

Au niveau micro, un analogue de la transition des organismes unicellulaires aux organismes multicellulaires peut être la transition des processeurs monocœur aux processeurs multicœurs (Chip MultiProcessors, CMP). CMP offre l'un des moyens de surmonter la faiblesse inhérente aux processeurs modernes - le goulot d'étranglement de l'architecture von Neumann.

C'est ce que disait John Backus lors de la cérémonie du prix Turing 1977: «Qu'est-ce qu'un ordinateur von Neumann? Lorsque John von Neumann et d'autres ont proposé leur architecture originale il y a 30 ans, l'idée semblait élégante, pratique et simplifiée pour résoudre une variété de problèmes d'ingénierie et de programmation. Et bien que les conditions qui existaient au moment de sa publication aient radicalement changé au fil des ans, nous identifions notre compréhension des ordinateurs avec ce vieux concept. Dans sa forme la plus simple, un ordinateur von Neumann se compose de trois parties: une unité centrale de traitement (CPU ou CPU), la mémoire et un canal les reliant, qui est utilisé pour échanger des données entre le CPU et la mémoire, et en petites parties (un seul mot chacune). Je propose d'appeler cette chaîne «le goulot d'étranglement de von Neumann». Il devrait sûrement y avoir une solution moins primitive que de pomper une énorme quantité de données à travers un goulot d'étranglement. Un tel canal crée non seulement un problème de trafic, mais constitue également un «goulot d'étranglement intellectuel» qui impose une réflexion «mot par mot» aux programmeurs, les empêchant de penser dans des catégories conceptuelles supérieures.

Backus était surtout connu pour la création au milieu des années 50 du langage Fortran, qui pendant les décennies suivantes fut l'outil le plus populaire pour créer des programmes de calcul. Mais plus tard, apparemment, Backus a profondément réalisé ses faiblesses et s'est rendu compte qu'il avait développé "la langue la plus von Neumann" de toutes les langues de haut niveau. Par conséquent, le principal pathétique de sa critique visait principalement les méthodes de programmation imparfaites.

Depuis que Backus a prononcé son discours, il y a eu des progrès significatifs dans la programmation, les technologies fonctionnelles et orientées objet, et avec leur aide, il a été possible de surmonter ce que Backus a appelé «le goulot d'étranglement intellectuel de von Neumann». Cependant, la cause architecturale de ce phénomène, la maladie congénitale du canal entre la mémoire et le processeur - sa bande passante limitée - n'a pas disparu, malgré les progrès technologiques au cours des 30 dernières années. Au fil des ans, ce problème n'a cessé de croître, car la vitesse de la mémoire augmente beaucoup plus lentement que les performances du processeur et l'écart entre eux se creuse.

L'architecture informatique de Von Neumann n'est pas la seule possible. Du point de vue de l'organisation de l'échange de commandes entre le processeur et la mémoire, tous les ordinateurs peuvent être divisés en quatre classes:

  • SISD (données uniques à instruction unique) - "un flux de commandes, un flux de données" ";
  • SIMD (données de multiplication à instruction unique) - un flux de commandes, de nombreux flux de données;
  • MISD (données uniques à instructions multiples) - de nombreux flux de commandes, un flux de données;
  • MIMD (données multiples à instructions multiples) - de nombreux flux de commandes, de nombreux flux de données.

Cette classification montre que la machine de von Neumann est un cas particulier appartenant à la catégorie SISD. Les améliorations potentielles au sein de l'architecture SISD se limitent à l'inclusion de pipelines et d'autres nœuds fonctionnels supplémentaires, ainsi qu'à l'utilisation de différentes méthodes de mise en cache. Deux autres catégories d'architectures (SIMD, qui comprend des processeurs vectoriels, et architectures en pipeline MISD) ont été mises en œuvre dans plusieurs projets, mais ne sont pas devenues courantes. Si nous restons dans le cadre de cette classification, alors le seul moyen de surmonter les limitations des goulots d'étranglement est le développement d'architectures de classe MIMD. Dans leur cadre, de nombreuses approches sont trouvées: il peut s'agir de différentes architectures parallèles et de cluster, et de processeurs multi-threads.

Il y a quelques années, en raison de limitations technologiques, tous les processeurs multithread étaient construits sur la base d'un seul cœur, et ce multithreading était appelé «simultané» Multithreading simultané (SMT)... Et avec l'avènement des processeurs multicœurs, un autre type de multithreading est apparu - Multiprocesseurs à puce (CMP).

Caractéristiques des processeurs multi-threads

La transition de simples processeurs monothreads vers des processeurs multi-threads logiquement plus complexes implique de surmonter des difficultés spécifiques qui n'avaient pas été rencontrées auparavant. Le fonctionnement d'un appareil, où le processus d'exécution est divisé en agents ou threads (threads), présente deux caractéristiques:

  • principe d'indétermination... Dans une application multithread, un processus est divisé en threads d'agent interagissant sans une définition prédéterminée;
  • principe incertain... La manière exacte dont les ressources seront allouées entre les threads d'agent est également inconnue à l'avance.

En raison de ces caractéristiques, le fonctionnement des processeurs multi-threads est fondamentalement différent des calculs déterministes selon le schéma de von Neumann. Dans ce cas, l'état actuel du processus ne peut pas être défini comme une fonction linéaire de l'état précédent et des données reçues en entrée, bien que chacun des processus puisse être considéré comme une micromachine de von Neumann. (Appliqué au comportement des threads, on peut même utiliser le terme «étrangeté» utilisé en physique quantique.) La présence de ces caractéristiques rapproche un processeur multithread du concept de système complexe, mais d'un point de vue purement pratique, il est clair qu'au niveau de l'exécution des processus il n'y a pas de non-déterminisme ou l'incertitude, et plus encore sur l'étrangeté et la parole ne peut pas être. Un programme exécuté correctement ne peut pas être étrange.

Dans sa forme la plus générale, un processeur multithread se compose de deux types de primitives. Le premier type est une ressource qui prend en charge l'exécution du flux, qui s'appelle un mutex (à partir de l'exclusion mutuelle), et le second est des événements. La manière dont tel ou tel mutex est physiquement implémenté dépend du schéma choisi - SMT ou CMP. Dans tous les cas, l'exécution du processus est réduite au fait que le thread suivant capture le mutex pendant la durée de son exécution, puis le libère. Si le mutex est occupé par un thread, le second thread ne peut pas l'obtenir. La procédure particulière pour déléguer la propriété mutex d'un thread à un autre peut être aléatoire; cela dépend de l'implémentation du contrôle, par exemple, dans un système d'exploitation particulier. Dans tous les cas, le contrôle doit être structuré de manière à ce que les ressources constituées de mutex soient correctement attribuées et que l'effet d'incertitude soit supprimé.

Les événements sont des objets (événement) qui signalent un changement dans l'environnement externe. Ils peuvent se mettre en attente jusqu'à ce qu'un autre événement se produise, ou signaler leur état à un autre événement. De cette manière, les événements peuvent interagir les uns avec les autres et, en même temps, la continuité des données entre les événements doit être assurée. Un agent en attente doit être informé que les données sont prêtes pour cela. Et comme dans la distribution du mutex, l'effet de l'incertitude doit être supprimé, de même lorsque l'on travaille avec des événements, l'effet de l'incertitude doit être supprimé. Pour la première fois, le schéma SMT a été implémenté dans les processeurs Compaq Alpha 21464, ainsi que dans Intel Xeon MP et Itanium.

Logiquement, CMP est plus simple: ici le parallélisme est assuré par le fait que chacun des threads est traité par son propre cœur. Mais si une application ne peut pas être threadée, elle (en l'absence de mesures spéciales) est traitée par un cœur, et dans ce cas, les performances totales du processeur sont limitées par la vitesse d'un cœur. À première vue, un processeur basé sur le schéma SMT est plus flexible, et donc ce schéma est préférable. Mais cette affirmation n'est vraie qu'avec une faible densité de transistors. Si la fréquence est mesurée en mégahertz et que le nombre de transistors dans le cristal approche le milliard et que les retards de transmission du signal deviennent plus longs que le temps de commutation, alors la microarchitecture CMP, dans laquelle les éléments de calcul associés sont localisés, en profite.

Cependant, la parallélisation physique rend CMP peu efficace pour les calculs séquentiels. Pour pallier cet inconvénient, une approche appelée «Multithreading spéculatif» est utilisée. En russe, le mot «spéculatif» a une connotation négative, nous appellerons donc un tel multithreading «conditionnel». Cette approche suppose un support matériel ou logiciel pour diviser une application séquentielle en threads conditionnels, coordonner leur exécution et intégrer les résultats en mémoire.

Évolution du CMP

Les premiers CMP grand public visaient le marché des serveurs. Quel que soit le fournisseur, il s'agissait essentiellement de deux processeurs superscalaires indépendants sur un même substrat. La principale motivation de ces conceptions est de réduire le volume afin que davantage de processeurs puissent être «emballés» dans une seule conception, augmentant la densité de puissance par unité de volume (ce qui est essentiel pour les centres de données modernes). Des économies supplémentaires sont alors réalisées au niveau du système global, puisque les processeurs sur la même puce utilisent des ressources système partagées telles que des communications à haut débit. Habituellement, il n'existe qu'une interface système commune entre les processeurs voisins ( figure. 1, b).

Les apologistes de l'utilisation des processeurs CMP justifient l'augmentation supplémentaire (plus de deux) du nombre de cœurs par les particularités de la charge du serveur, qui distingue ce type d'ordinateur des systèmes informatiques embarqués ou massifs. De bonnes performances globales sont requises du serveur, mais la latence d'un appel individuel au système n'est pas si critique. Pour un exemple trivial, un utilisateur peut simplement ne pas remarquer le délai de quelques millisecondes dans l'apparition d'une page Web actualisée, mais il est très sensible aux surcharges de serveur qui peuvent provoquer des interruptions de service.

La charge de travail spécifique donne aux processeurs CMP un autre avantage notable. Par exemple, en remplaçant un processeur monocœur par un processeur bicœur, vous pouvez diviser par deux la fréquence d'horloge avec les mêmes performances. Dans ce cas, théoriquement, le temps de traitement d'une requête individuelle peut doubler, mais comme la séparation physique des flux réduit la contrainte de goulot d'étranglement de l'architecture von Neumann, le délai total sera bien inférieur à deux fois. Avec une fréquence et une complexité inférieures d'un cœur, la consommation d'énergie est considérablement réduite, et avec une augmentation du nombre de cœurs, les arguments ci-dessus en faveur du CMP ne font que se renforcer. Par conséquent, la prochaine étape logique consiste à collecter plusieurs cœurs et à les combiner avec une mémoire cache commune, par exemple, comme dans le projet Hydra (Figure 1, c). Et puis vous pouvez compliquer les cœurs et les rendre multi-threads, ce qui a été implémenté dans le projet Niagara (Figure 1, d).

La complexité des processeurs a une autre manifestation importante. Concevoir un produit avec des milliards de composants devient une tâche de plus en plus laborieuse - malgré l'utilisation de l'automatisation. Il est significatif que nous assistions à plus d'une décennie de «peaufinage» de l'architecture IA-64. La conception d'un processeur CMP est beaucoup plus simple: s'il existe un cœur bien développé, il peut être répliqué dans les quantités requises, et la conception se limite à la création de l'infrastructure interne du cristal. De plus, l'uniformité des cœurs simplifie la conception des cartes mères, qui se résume à la mise à l'échelle, et finalement, les paramètres des sous-systèmes d'E / S changent.

Malgré les arguments ci-dessus, il n'y a toujours pas de base suffisante pour une déclaration sans ambiguïté sur les avantages de CMP par rapport à SMT. L'expérience de création de processeurs mettant en œuvre SMT est bien plus grande: depuis le milieu des années 80, plusieurs dizaines de produits expérimentaux et plusieurs processeurs série ont été créés. L'histoire du développement du CPM est encore courte: si vous ne prenez pas en compte la famille de processeurs de signaux spécialisés Texas Instruments TMS 320C8x, le premier projet réussi a été Hydra, réalisé à l'Université de Stanford. Parmi les projets de recherche universitaire visant à construire des processeurs CMP, trois autres sont connus - Wisconsin Multiscalar, Carnegie-Mellon Stampede et MIT M-machine.

Microprocesseur Hydra

Le cristal Hydra se compose de quatre cœurs de processeur basés sur l'architecture bien connue MIPS RISC. Chaque cœur dispose d'un cache d'instructions et d'un cache de données, et tous les cœurs sont combinés dans un cache L2 partagé. Les processeurs exécutent le jeu d'instructions MIPS habituel plus les instructions Store Conditional ou SC pour implémenter les primitives de synchronisation. Les processeurs et le cache L2 sont connectés par des bus de lecture / écriture, et en plus, il existe des bus d'adresse et de contrôle auxiliaires. Tous ces bus sont virtuels, c'est-à-dire qu'ils sont logiquement représentés par des bus filaires, et sont physiquement divisés en plusieurs segments à l'aide de répéteurs et de tampons, ce qui permet d'augmenter la vitesse des cœurs.

Le bus de lecture / écriture joue le rôle d'un bus système. En raison de son emplacement à l'intérieur du cristal, il a suffisamment débit pour assurer l'échange avec la mémoire cache en un seul cycle. Il est difficile d'atteindre de tels indicateurs de performance d'échange même dans les architectures multiprocesseurs traditionnelles les plus coûteuses en raison des limitations physiques sur le nombre de contacts de processeurs externes. Des bus de cache efficaces évitent les goulots d'étranglement entre les cœurs et la mémoire.

Le test de l'Hydra sous des charges de travail fortement parallèles sur des applications Web et serveur typiques a montré que les performances de quatre cœurs par rapport à un cœur augmentaient de 3 à 3,8 fois, c'est-à-dire presque linéairement. Cela donne des raisons de croire que les processeurs de ce type "s'intégreront" avec succès dans les applications qui utilisent des serveurs avec une architecture SMP. Mais il est clair que le processeur doit fonctionner assez efficacement avec des applications séquentielles, donc l'une des tâches les plus importantes est d'implémenter le multithreading conditionnel. Dans Hydra, il est implémenté en matériel, et le choix de cette approche se justifie par le fait qu'elle ne nécessite pas de coûts supplémentaires pour la programmation d'applications parallèles.

Le multithreading conditionnel est basé sur la division d'une séquence de commandes de programme en threads pouvant être exécutés en parallèle. Naturellement, il peut y avoir une interdépendance logique entre ces threads et un mécanisme de synchronisation spécial est intégré au processeur pour les coordonner. L'essence de son travail se résume au fait que si un thread nécessite des données d'un thread parallèle, et qu'ils ne sont pas encore prêts, alors l'exécution d'un tel thread est suspendu. En fait, les éléments du non-déterminisme, qui ont été discutés ci-dessus, se manifestent ici. Le processus de synchronisation est assez complexe, car toutes les dépendances possibles entre les threads et les conditions de synchronisation doivent être déterminées. La synchronisation conditionnelle permet aux programmes d'être parallélisés sans connaissance préalable de leurs propriétés. Il est important que le mécanisme de synchronisation soit dynamique, il fonctionne sans l'intervention d'un programmeur ou d'un compilateur, qui n'est capable que de diviser statiquement les applications en threads. Des tests du modèle basés sur différents tests ont montré que les moyens de multithreading conditionnel peuvent multiplier par plusieurs les performances du processeur, et plus le parallélisme du test est explicite, plus cette valeur est faible.

En 2000, Afara a été établi dans un environnement hautement secret. Ses fondateurs étaient le professeur Kunle Olukotun de l'Université de Stanford et le célèbre concepteur de processeurs Les Cohn, qui avait travaillé chez Intel et Sun Microsystems. Cohn a été l'un des auteurs des processeurs RISC i860 et i960 dans la première de ces sociétés et UltraSPARC-I dans la seconde. Sous sa direction, Hydra a été repensé pour les cœurs de processeur SPARC. En 2002, Afara a été racheté par Sun Microsystems, et c'était la fin de l'histoire d'Hydra et le début de l'histoire de Niagara.

Niagara - "fusion" de MAJC et Hydra

Le processeur UltraSPARC T1, mieux connu sous le nom de Niagara, a deux prédécesseurs principaux, l'Hydra et le MAJC.

Au milieu des années 1990, suite à l'engouement pour les processeurs Java spécialisés, Sun Microsystems a tenté de créer un processeur VLIW (Very Long Instruction Word). Cette initiative a été baptisée MAJC (Microprocessor Architecture for Java Computing). Comme dans d'autres projets qui ont démarré à cette époque (Intel IA-64 Itanium), dans ce cas, la tâche consistait à transférer certaines des opérations les plus complexes vers le compilateur. La logique de transistor libérée peut être utilisée pour créer des unités fonctionnelles plus efficaces afin de fournir un échange efficace d'instructions et de données entre le CPU, la mémoire cache et la mémoire principale. Ainsi, le goulot d'étranglement de von Neumann a été surmonté.

MAJC diffère de la plupart des processeurs en l'absence de coprocesseurs spécialisés (sous-processeurs), généralement appelés dispositifs fonctionnels conçus pour effectuer des opérations avec des nombres entiers, des nombres à virgule flottante et des données multimédias. Dans celui-ci, tous les appareils fonctionnels étaient les mêmes, capables d'effectuer toutes les opérations, ce qui, d'une part, réduisait l'efficacité des opérations individuelles, mais, d'autre part, augmentait l'utilisation de l'ensemble du processeur.

Niagara incarne la meilleure des deux approches alternatives du multithreading - SMT et CMP. À première vue, cela ressemble beaucoup à l'Hydra, mais plutôt, l'Hydra peut être qualifiée de "mock" de Niagara. Outre le fait que ce dernier possède deux fois plus de noyaux, chacun d'eux peut gérer quatre fils.

Le processeur Niagara fournit un support matériel pour 32 threads, qui sont divisés en huit groupes (quatre threads chacun). Chaque groupe possède son propre pipeline SPARC ( fig.2). Il s'agit d'un cœur de processeur construit selon l'architecture SPARC V9. Chaque pipeline SPARC contient un cache de premier niveau pour les commandes et les données. Ensemble, 32 threads partagent un cache L2 de 3 Mo divisé en quatre banques. Le commutateur connecte huit cœurs, des banques de cache L2 et d'autres ressources CPU allouées, et prend en charge un taux de transfert de 200 Go / s. De plus, le commutateur contient un port pour les systèmes d'E / S et des canaux vers la mémoire de type DRAM DDR2, offrant un taux de change de 20 Go / s; la capacité de mémoire maximale est de 128 Go.

Le projet Niagara est axé sur le système d'exploitation Solaris, de sorte que toutes les applications exécutées sur Solaris peuvent s'exécuter sur le nouveau processeur sans aucune modification. Le logiciel d'application interprète Niagara comme 32 processeurs discrets.

Projet de cellule

Sa propre approche de la création de processeurs multicœurs a été proposée par IBM Corporation, dont le projet Cell a été qualifié de "multiprocesseur à puce hétérogène". L'architecture de cellule est également appelée architecture de moteur à large bande cellulaire (CBEA). Le multiprocesseur Cell se compose d'un cœur IBM Power Architecture 64 bits et de huit coprocesseurs spécialisés qui implémentent un schéma à une instruction plusieurs données. IBM appelle cette architecture le Synergistic Processor Unit (SPU). Il peut être utilisé avec succès lorsqu'il est nécessaire de traiter de gros flux de données, par exemple en cryptographie, dans diverses applications multimédias et scientifiques telles que la transformation rapide de Fourier ou les opérations matricielles. L'architecture Cell a été créée par une équipe de chercheurs d'IBM Research avec des collègues d'IBM Systems Technology Group, Sony et Toshiba, et sa première application devrait être des périphériques multimédias qui nécessitent de grandes quantités de calcul.

L'unité de processeur synergique est basée sur le jeu d'instructions ISA (Instruction Set Architecture). Les instructions ont une longueur de 32 bits et sont adressées à trois opérandes situés dans le pool de registres, qui se compose de 128 registres de 128 bits chacun.

À l'avenir, l'utilisation de Cell ne se limitera pas aux systèmes de jeu. La télévision haute définition, les serveurs domestiques et même les supercalculateurs viennent ensuite.

Littérature
  1. Leonid Chernyak. Révision des premiers principes - la fin de la stagnation? // Systèmes ouverts. - 2003, n ° 5.
  2. Mikhail Kuzminsky. Architecture multithread de microprocesseurs // Systèmes ouverts. - 2002, n ° 1.
  3. Rajat A Dua, Bhushan Lokhande. Une étude comparative des multiprocesseurs SMT et CMP. -

Après avoir traité de la théorie du multithreading, considérons un exemple pratique - le Pentium 4. Déjà au stade de développement de ce processeur, les ingénieurs d'Intel ont continué à travailler sur l'augmentation de ses performances sans introduire de changements dans l'interface du programme. Cinq moyens les plus simples ont été envisagés:
1. Augmentation de la fréquence d'horloge.
2. Placement de deux processeurs sur un microcircuit.
3. Introduction de nouveaux blocs fonctionnels.
1. Extension du convoyeur.
2. Utilisation du multithreading.
La manière la plus évidente d'améliorer les performances consiste à augmenter la vitesse d'horloge sans modifier les autres paramètres. En règle générale, chaque modèle de processeur suivant a une vitesse d'horloge légèrement plus élevée que le précédent. Malheureusement, avec une simple augmentation de la fréquence d'horloge, les développeurs sont confrontés à deux problèmes: une augmentation de la consommation d'énergie (ce qui est important pour les ordinateurs portables et autres appareils informatiques fonctionnant sur batteries) et la surchauffe (qui nécessite des dissipateurs de chaleur plus efficaces).
La deuxième méthode - placer deux processeurs sur un microcircuit - est relativement simple, mais elle consiste à doubler la surface occupée par le microcircuit. Si chaque processeur est fourni avec sa propre mémoire cache, le nombre de puces sur un plateau est divisé par deux, mais cela signifie également un doublement des coûts de production. En fournissant un cache partagé pour les deux processeurs, une augmentation significative de l'encombrement peut être évitée, mais dans ce cas, un autre problème se pose: la quantité de mémoire cache par processeur est divisée par deux, ce qui affecte inévitablement les performances. En outre, alors que les applications serveur professionnelles sont capables d'utiliser pleinement les ressources de plusieurs processeurs, dans les programmes de bureau ordinaires, le parallélisme interne est beaucoup moins développé.
L'introduction de nouveaux blocs fonctionnels n'est pas non plus difficile, mais il est important de trouver un équilibre ici. Quel est l'intérêt d'une douzaine de blocs ALU si le microcircuit ne peut pas envoyer de commandes au convoyeur à une vitesse telle qu'il puisse charger tous ces blocs?
Un convoyeur avec un nombre accru d'échelons, capable de diviser les tâches en segments plus petits et de les traiter en peu de temps, d'une part, augmente la productivité, d'autre part, augmente les conséquences négatives des transitions mal prévues, des échecs de cache, des interruptions et d'autres événements qui perturbent le flux normal traitement des commandes dans le processeur. De plus, pour réaliser pleinement les capacités du pipeline étendu, il est nécessaire d'augmenter la fréquence d'horloge, ce qui, comme nous le savons, entraîne une augmentation de la consommation d'énergie et de la dissipation thermique.
Enfin, vous pouvez implémenter le multithreading. L'avantage de cette technologie est qu'elle introduit un thread logiciel supplémentaire pour activer des ressources matérielles qui seraient autrement inactives. Sur la base des résultats d'études expérimentales, les développeurs Intel ont constaté qu'une augmentation de 5% de la surface de la puce lors de la mise en œuvre du multithreading pour de nombreuses applications se traduit par un gain de performances de 25%. Le premier processeur Intel à prendre en charge le multithreading était le Xeon 2002. Par la suite, à partir de 3,06 GHz, le multithreading a été introduit dans la ligne Pentium 4. Intel appelle l'implémentation du multithreading dans l'hyperthreading Pentium 4.
Le principe de base de l'hyper-threading est l'exécution simultanée de deux threads de programme (ou processus - le processeur ne fait pas la distinction entre les processus et les threads de programme). Le système d'exploitation considère le processeur hyper-thread Pentium 4 comme un complexe à deux processeurs avec des caches partagés et de la mémoire principale. Le système d'exploitation effectue la planification pour chaque thread de programme séparément. Ainsi, deux applications peuvent s'exécuter en même temps. Par exemple, un mailer peut envoyer ou recevoir des messages en arrière-plan pendant que l'utilisateur interagit avec une application interactive, c'est-à-dire que le démon et le programme utilisateur s'exécutent simultanément comme s'il y avait deux processeurs disponibles sur le système.
Les programmes d'application qui peuvent être exécutés comme plusieurs threads de programme peuvent utiliser les deux "processeurs virtuels". Par exemple, les programmes de montage vidéo permettent généralement aux utilisateurs d'appliquer des filtres à toutes les images. Ces filtres ajustent la luminosité, le contraste, la balance des couleurs et d'autres propriétés des cadres. Dans une telle situation, le programme peut affecter un processeur virtuel pour traiter les trames paires et un autre pour traiter les trames impaires. Dans ce cas, les deux processeurs fonctionneront complètement indépendamment l'un de l'autre.
Puisque les threads logiciels accèdent aux mêmes ressources matérielles, la coordination de ces threads est nécessaire. Dans le contexte de l'hyperthreading, Intel a identifié quatre stratégies utiles pour gérer le partage des ressources: la duplication des ressources et le partage des ressources matérielles, à seuil et complet. Considérons ces stratégies.
Commençons par la duplication des ressources. Comme vous le savez, certaines ressources sont dupliquées dans le but d'organiser les flux de programmes. Par exemple, puisque chaque thread de programme nécessite un contrôle individuel, un deuxième compteur d'instructions est nécessaire. De plus, il est nécessaire de saisir une deuxième table de mappage des registres architecturaux (EAX, EBX, etc.) aux registres physiques; De même, le contrôleur d'interruption est dupliqué, car la gestion des interruptions pour chaque thread est effectuée individuellement.
Ce qui suit est une technique de partage de ressources partitionnées entre les flux de programmes. Par exemple, si le processeur fournit une file d'attente entre deux étages fonctionnels du pipeline, alors la moitié des emplacements peut être attribuée au thread 1, l'autre moitié au thread 2. Le partage des ressources est facile à mettre en œuvre, n'entraîne pas de déséquilibre et garantit une indépendance totale des threads du programme les uns des autres. Avec le partage complet de toutes les ressources, un processeur se transforme en deux. D'un autre côté, une situation peut se produire dans laquelle un thread de programme n'utilise pas de ressources qui pourraient être utiles au second thread, mais pour lesquelles il n'a pas les droits d'accès. Par conséquent, les ressources qui pourraient autrement être utilisées sont inactives.
Le contraire du partage dur est le partage complet des ressources. Dans ce schéma, n'importe quel thread de programme peut accéder aux ressources nécessaires et elles sont servies dans l'ordre dans lequel les demandes d'accès sont reçues. Prenons une situation dans laquelle un thread rapide, composé principalement d'opérations d'addition et de soustraction, coexiste avec un thread lent qui implémente des opérations de multiplication et de division. Si les instructions sont appelées de la mémoire plus rapidement que les opérations de multiplication et de division sont effectuées, le nombre d'instructions appelées dans le thread lent et mises en file d'attente dans le pipeline augmentera progressivement. En fin de compte, ces commandes rempliront la file d'attente, par conséquent, le flux rapide s'arrêtera en raison du manque d'espace. Le partage complet des ressources résout le problème de l'utilisation non optimale des ressources partagées, mais crée un déséquilibre dans leur consommation - un thread peut ralentir ou en arrêter un autre.
Le schéma intermédiaire est mis en œuvre dans le cadre du partage de ressources à seuil. Selon ce schéma, n'importe quel thread de programme peut acquérir dynamiquement une certaine quantité (limitée) de ressources. Lorsqu'elle est appliquée à des ressources répliquées, cette approche offre une flexibilité sans menace de temps d'arrêt pour l'un des threads du programme en raison de l'incapacité d'obtenir des ressources. Si, par exemple, vous interdisez à chacun des threads d'occuper plus des 3/4 de la file de commandes, l'augmentation de la consommation de ressources par un thread lent n'empêchera pas l'exécution d'un thread rapide.
Le modèle d'hyperthreading du Pentium 4 combine différentes stratégies de partage de ressources. Ainsi, on tente de résoudre tous les problèmes associés à chaque stratégie. La duplication est mise en œuvre en relation avec des ressources constamment sollicitées par les deux threads de programme (en particulier, en relation avec le compteur d'instructions, la table de mappage des registres et le contrôleur d'interruption). La duplication de ces ressources augmente la surface du microcircuit de seulement 5% - d'accord, un paiement tout à fait raisonnable pour le multithreading. Les ressources disponibles dans un volume tel qu'il est presque impossible pour elles d'être capturées par un thread (par exemple, les lignes de cache) sont allouées dynamiquement. L'accès aux ressources qui contrôlent le fonctionnement du pipeline (en particulier, ses nombreuses files d'attente) est divisé - la moitié des emplacements sont affectés à chaque thread de programme. Le pipeline principal de l'architecture Pentium 4 Netburst est illustré à la Fig. 8,7; blanc et zones grises dans cette illustration, désignent le mécanisme d'allocation de ressources entre les flux de programme blanc et gris.
Comme vous pouvez le voir, toutes les files d'attente de cette illustration sont séparées - chaque thread de programme se voit attribuer la moitié des emplacements. Aucun fil ne peut restreindre le travail de l'autre. Le bloc de distribution et de remplacement est également divisé. Les ressources du planificateur sont partagées dynamiquement, mais en fonction d'un certain seuil, de sorte qu'aucun thread ne peut occuper tous les emplacements de la file d'attente. Pour toutes les autres étapes du convoyeur, une séparation complète a lieu.
Cependant, le multithreading n'est pas si simple. Même cette technique progressive présente des inconvénients. Le partage rigide des ressources n'entraîne pas de surcharge importante, mais le partitionnement dynamique, en particulier en ce qui concerne les seuils, nécessite le suivi de la consommation des ressources au moment de l'exécution. De plus, dans certains cas, les programmes fonctionnent bien mieux sans le multithreading qu'avec lui. Par exemple, supposons que si vous avez deux threads, ils nécessitent chacun 3/4 du cache pour fonctionner correctement. S'ils étaient exécutés à leur tour, chacun montrerait une efficacité suffisante avec un petit nombre d'échecs de cache (comme vous le savez, associés à des coûts supplémentaires). Dans le cas d'une exécution parallèle, chacun aurait significativement plus d'erreurs de cache, et le résultat final serait pire que sans le multithreading.
Pour plus d'informations sur le mécanisme multithreading de RepPit 4, voir.

LA CLOCHE

Il y a ceux qui ont lu cette nouvelle avant vous.
Abonnez-vous pour recevoir les derniers articles.
Email
Nom
Nom de famille
Comment voulez-vous lire The Bell
Pas de spam