Matériel
Les différents fabricants de matériel indépendants (IHV) définissent les topologies de processeurs hétérogènes de manière différente. Intel et Qualcomm utilisent les termes Performance et Efficiency, tandis qu’AMD utilise les termes Performance et Compact. Chaque fabricant fait des compromis entre performances et efficacité lors de la conception de sa topologie de cœurs. Les exemples suivants illustrent certains des choix de conception en jeu :- Tailles de cache différentes
- Les différents cœurs peuvent avoir des tailles de cache différentes.
- Par exemple, les cœurs performants peuvent disposer de caches L1 et L2 plus grands.
- Partage du cache
- Les caches peuvent être partagés entre les cœurs à des degrés divers.
- Par exemple, les cœurs les plus performants peuvent disposer chacun d’un cache L2 privé, tandis que les cœurs plus efficaces peuvent partager un seul cache L2.
- Prise en charge du multithreading simultané (SMT)
- Certains cœurs peuvent prendre en charge le SMT, d’autres non.
- Intel désigne le SMT sous le nom d’Hyper-Threading.
- Organisation interne des cœurs
- Les ressources internes des différents types de cœurs peuvent varier.
- Par exemple, les cœurs performants peuvent comporter davantage de pipelines à virgule flottante, de pipelines ALU ou des voies de données plus larges.
- Fréquence maximale
- La fréquence maximale peut différer selon le type de cœur.
- Par exemple, les cœurs performants peuvent atteindre une fréquence plus élevée que les cœurs efficaces.
Chiffres de performances
On s’attend à ce que des cœurs de niveaux d’efficacité différents offrent des performances différentes. L’un est conçu pour les performances, l’autre pour les économies d’énergie. La question est de savoir dans quelle mesure ces cœurs diffèrent. Les données présentées regroupent plusieurs IHV. Chaque IHV présente une mise à l’échelle différente pour chaque test, en fonction des choix effectués pour sa topologie. Si les chiffres exacts diffèrent, les tendances sont similaires chez tous les IHV. En outre, chaque génération de processeurs fait des compromis différents, ce qui modifie les chiffres relatifs. Produire des chiffres pour l’ensemble des IHV et des générations existants dépasse le cadre de cet article.Tests synthétiques
Les calculs vectoriels représentent une part importante du travail effectué par un titre de jeu au cours d’une image. Cette charge de travail peut concerner aussi bien la physique que l’IA ou le rendu. Le jeu effectue ce travail à l’aide de types vectoriels de trois ou quatre nombres à virgule flottante qui tiennent dans un seul registre SSE de 128 bits. Ils couvrent un large éventail d’opérations, telles que les produits scalaires, la multiplication de matrices, les calculs de racine carrée et les transformations de vecteurs. Les différences de performances sur ces opérations peuvent avoir un effet significatif sur le temps d’image global. Les graphiques suivants montrent le coût relatif de sept opérations mathématiques courantes. Les mesures utilisent un vecteur de quatre valeurs à virgule flottante de 32 bits stockées dans un seul registre SSE de 128 bits. Une matrice est composée de quatre vecteurs, soit un total de seize valeurs à virgule flottante de 32 bits réparties sur quatre registres SSE de 128 bits. Ces opérations ont été choisies parce qu’elles sont couramment utilisées au cours d’une image.- Produit scalaire : calcule le produit scalaire d’un vecteur.
- Transformation de vecteur : transforme un vecteur en le multipliant par une matrice.
- Addition de vecteurs : additionne deux vecteurs.
- Produit vectoriel : calcule le produit vectoriel de deux vecteurs.
- Sinus vectoriel : calcule une approximation du sinus de chaque élément d’un vecteur.
- FMA vectoriel (multiplication-addition fusionnée) : multiplie deux vecteurs et ajoute un troisième.
- Racine carrée inverse : calcule l’inverse de la racine carrée de la longueur d’un vecteur.

Conditions réelles
Les chiffres précédents présentent des bancs d’essai spécifiques pris isolément et ne racontent qu’une partie de l’histoire. Les chiffres suivants proviennent de bancs d’essai d’un moteur de jeu AAA qui utilise deux threads principaux (Simulation et Rendu) ainsi qu’un ensemble de threads de tâches. Le banc d’essai utilise trois configurations de test.- Les deux threads principaux s’exécutent uniquement sur les cœurs les plus performants.
- Les deux threads principaux s’exécutent uniquement sur les cœurs efficaces.
- Les deux threads principaux s’exécutent sur tous les cœurs du système.

Chaque fabricant peut ajuster ces heuristiques du planificateur pour les adapter à son matériel. Par exemple, AMD propose sa technologie Workload Profile Scheduling et Intel sa technologie Thread Director. Chaque IHV a également tendance à ajuster les heuristiques en fonction de la topologie spécifique du SoC.
Recommandations
Affinité des threads
Sur un ordinateur de bureau ne disposant que d’une seule classe d’efficacité, il est conseillé d’éviter de définir des affinités de threads strictes et de laisser plutôt les threads se déplacer sur tous les cœurs. La principale raison de cette incertitude est qu’on ne peut pas savoir à l’avance quels autres processus peuvent s’exécuter sur la machine d’un utilisateur. Un thread doté d’une affinité stricte peut finir par partager un cœur avec un autre thread de priorité élevée du système. Au final, le thread du titre est privé de temps d’exécution. Si cette contention se produit sur un thread critique pour l’image, le titre peut présenter des temps d’image irréguliers. Vous devez vous assurer que le système d’exploitation dispose d’un endroit où déplacer les threads du titre pour qu’ils continuent de s’exécuter. Cependant, ce conseil doit être revu pour les processeurs hétérogènes. La raison principale est que le système d’exploitation ne sait pas nécessairement quels threads ou tâches du titre sont critiques pour l’image. Il dispose de certaines heuristiques pour déterminer ce qu’il considère comme les threads critiques pour l’image, mais ces heuristiques peuvent tout de même faire de mauvais choix. Il peut décider de déplacer l’un de ces threads ou l’une de ces tâches vers un cœur moins performant, ce qui augmente la probabilité de problèmes de performances dans le titre. Il est recommandé de permettre aux threads du titre de s’exécuter sur au moins trois à quatre cœurs, et davantage si possible. Par exemple, sur un système d’entrée de gamme doté d’un cœur plus performant et de trois cœurs moins performants, vous devez permettre aux threads de se déplacer sur tous les cœurs. Sur les machines dotées d’au moins trois cœurs performants, vous pouvez limiter les threads critiques pour l’image à ces seuls cœurs plus performants. Sur les machines haut de gamme dotées de nombreux cœurs qui prennent également en charge le SMT, certains titres peuvent essayer de verrouiller leurs threads sur des cœurs physiques. Cette technique évite la contention au niveau du cœur physique lorsque plusieurs threads de priorité élevée partagent les mêmes ressources d’un cœur physique. En général, il n’est toutefois pas recommandé d’envisager ce cas. Le système d’exploitation applique déjà ce comportement à l’échelle du système : il répartit d’abord les threads sur les cœurs physiques et n’utilise davantage de cœurs logiques qu’en cas de nécessité. Le seul cas qu’un titre devrait envisager est celui où le système dispose de suffisamment de cœurs plus performants pour prendre en charge les threads critiques du titre. Dans ce cas, il peut être souhaitable de verrouiller les threads critiques pour l’image sur les cœurs plus performants. Toutefois, ces threads doivent pouvoir se déplacer sur l’ensemble des cœurs plus performants. Si, dans certains cas, permettre à tous les threads de se déplacer sur tous les cœurs peut améliorer les performances, ce comportement n’est pas homogène chez tous les fabricants de processeurs. Si le titre implémente un système de vol de travail (work stealing) capable d’équilibrer entièrement la charge de travail entre tous les threads, tous les threads de tâches doivent pouvoir se déplacer sur tous les cœurs, qu’il s’agisse de threads de tâches de priorité élevée ou faible. L’algorithme de vol de travail équilibre automatiquement la charge et permet d’effectuer le plus de travail possible dans le temps le plus court. Les threads situés sur les cœurs plus performants exécutent davantage de tâches au cours de l’image. S’il existe plusieurs threads critiques pour l’image, il peut être utile de définir leur affinité de manière à ce que deux threads critiques ne puissent jamais s’exécuter sur le même cœur. Par exemple, s’il existe deux threads critiques pour l’image, définir leurs affinités de façon mutuellement exclusive (OU exclusif) élimine la possibilité que le système d’exploitation les mette en concurrence. Là encore, n’envisagez cette option que s’il y a suffisamment de cœurs pour la prendre en charge tout en permettant à chaque thread critique pour l’image de se déplacer sur au moins trois à quatre cœurs.Topologie des threads
Lorsque vous déterminez comment attribuer le travail aux threads dans un environnement hétérogène, les données principales portent sur la durée des différentes tâches et sur leurs interactions : durée moyenne de chaque tâche, dépendances entre les tâches, priorités de chaque tâche, etc. Dans quelle mesure les threads de tâches peuvent-ils absorber les pics de temps sur les threads ou tâches critiques ? Si l’un de ces threads ou l’une de ces tâches s’exécute 35 % plus lentement, quel est l’effet sur le temps d’image ? Par exemple, si une tâche dure plus longtemps que prévu, dans quelle mesure ce retard affecte-t-il le reste de l’image ? Le système est-il bloqué jusqu’à la fin de cette tâche ? Il peut être utile de déterminer dans quelle mesure un système hétérogène affecte le temps d’image en ajoutant une surcharge supplémentaire dans les threads ou tâches critiques. À titre de test, faites en sorte qu’une tâche ou un bloc de travail choisi au hasard prenne 25 % à 35 % de temps en plus. L’ampleur de la variation du temps d’image global indique la capacité du système de tâches à absorber les pics d’exécution. Idéalement, les temps d’image ne varient presque pas, ce qui signifie que le système peut absorber des pics arbitraires de temps d’image. Ces pics proviennent soit d’une exécution sur un cœur moins performant, soit d’un autre processus quelconque du système qui utilise du temps processeur.Communication entre threads
La communication entre threads au moyen de primitives de verrouillage mérite réflexion. La surcharge d’un changement de contexte est étroitement liée à l’état C (C-State) du cœur récepteur avant que le nouveau thread ne commence à s’exécuter. Le cœur doit être en C0, l’état d’exécution, avant que le nouveau thread puisse démarrer. Le cas le moins coûteux est celui où le cœur était déjà en cours d’exécution, donc déjà en C0. En revanche, si le cœur est inactif, il se trouve dans un état C inférieur. Le niveau de l’état C correspond à des niveaux de veille plus profonds. Une veille plus profonde réduit la consommation d’énergie du cœur, mais il est plus coûteux de sortir d’une veille profonde pour recommencer à s’exécuter. L’état C basse consommation le plus courant est C1, qui peut ajouter une surcharge de 2 à 7 μs. L’état C suivant, C2, peut ajouter une surcharge d’environ 40 à 100 μs. Le chiffre exact dépend de l’IHV et du SoC. De nombreuses variables déterminent l’état C d’un cœur inactif ; il s’agit d’un équilibre entre réactivité et consommation d’énergie. Un état C plus bas consomme moins d’énergie qu’un état C plus élevé. Il est courant qu’un cœur entre d’abord en C1, puis passe en C2 s’il n’a pas exécuté de code depuis un certain temps. Une autre question se pose : que se passe-t-il lorsque le thread qui soumet la tâche et le thread qui l’exécute se trouvent sur des cœurs aux profils de performances différents ? Techniquement, le thread de tâches peut terminer les tâches plus vite que les nouvelles tâches ne sont créées. Cette situation est plus probable si le thread qui exécute la tâche se trouve sur un cœur plus rapide que le thread qui soumet les tâches. Si une tâche prend moins de temps à s’exécuter qu’à être créée, le titre peut se retrouver dans une situation où un thread de tâches se suspend constamment dans l’attente de nouvelles tâches. Au final, les tâches deviennent plus coûteuses à exécuter, car le thread de tâches se suspend constamment. Le coût d’un changement de contexte s’ajoute au temps d’exécution de chaque tâche. Paradoxalement, cette surcharge peut entraîner une baisse de la fréquence d’images lors de l’exécution sur des cœurs plus performants. Par exemple, un travail qui prenait 250 μs peut soudainement prendre 275 μs, soit une baisse de performances de 10 %. Pour cette raison, il est recommandé de soumettre les tâches par lots plutôt qu’au fur et à mesure de leur création. Par exemple, si le système suit un modèle dans lequel il construit une tâche, la soumet, construit une tâche, la soumet, etc., ce modèle doit être modifié pour construire d’abord un lot de tâches, puis les soumettre toutes en même temps. Si le titre utilise un objet qui nécessite WaitForSingleObject, cet appel doit être placé dans une petite boucle d’attente active (spin). Appelez la fonction avec un délai d’expiration de 0, puis, une fois l’attente active terminée, utilisez la valeur de délai d’expiration habituelle du titre. La meilleure solution à ce problème reste toutefois d’utiliser des API de synchronisation modernes comme les verrous SRW (Slim Reader/Writer), les sections critiques et WaitOnAddress. Ces API de synchronisation intègrent toutes une forme d’attente active en interne, ce qui peut éviter qu’un thread ne se suspende constamment. Cette attente active est particulièrement utile pour les verrous détenus pendant une courte durée. Il est important d’éviter les verrous à attente active prolongée, par exemple un modèle dans lequel les threads tournent constamment en attente de travail à effectuer. Si un autre travail doit être effectué sur le cœur, le planificateur finit par suspendre le thread en attente active pour effectuer ce travail. Lorsque cette préemption se produit, il est très probable que le thread suspendu le reste pendant une durée importante. Ce comportement vise à éviter la privation des autres threads. Comme le titre n’a que peu de contrôle sur le moment de la suspension, cela peut entraîner des saccades de la fréquence d’images.Systèmes de tâches
Pour des performances optimales, structurez le moteur autour de tâches plutôt que de vous appuyer sur des threads dédiés, critiques pour l’image et de longue durée. Par exemple, un modèle de threads Simulation/Rendu crée deux threads persistants critiques pour l’image. Cependant, des tâches longues peuvent amener n’importe quel thread de tâches à se comporter comme un thread critique pour l’image. Si une tâche prend 10 ms, le thread qui l’exécute devient critique pour l’image pendant cette période, quel que soit son rôle initial.Vol de travail
La recommandation principale est d’utiliser le vol de travail dans l’ensemble du système de tâches et de faire participer tous les threads critiques pour l’image à l’algorithme de vol de travail. Par exemple, si le thread de rendu attend la fin de tâches liées au rendu, il doit exécuter certaines de ces tâches. Ce comportement est déjà une pratique courante dans de nombreux moteurs de jeu. Il mérite toutefois d’être mentionné ici, car il est essentiel pour que le moteur de jeu puisse absorber les pics de temps d’image dus aux cœurs hétérogènes et à une éventuelle préemption par le système d’exploitation. Plus une tâche met de temps à s’exécuter, plus le risque de pic de temps d’image augmente. Ce comportement peut être dû soit à l’exécution de la tâche sur un cœur moins performant, soit à d’autres travaux sur le PC. Il est recommandé que le moteur vise des tâches représentant environ deux pour cent de l’image. Ce budget correspond, par exemple, à environ 333 μs au maximum pour un jeu s’exécutant à 60 i/s. Si nécessaire, plusieurs petites tâches doivent être regroupées en une tâche plus grande afin d’atteindre cet objectif de deux pour cent. Cette approche offre un bon compromis entre la taille des tâches, la communication entre threads et la capacité à absorber les pics. Ces exemples montrent les effets que des tâches longues critiques pour l’image peuvent avoir sur les performances du système. Dans ces exemples, chaque tâche comporte le même nombre d’opérations. Ici, une tâche unique prend 375 μs sur un cœur lent et 250 μs sur un cœur rapide. La tâche goulot d’étranglement est 6 fois plus coûteuse que les autres tâches. Le pire scénario est celui où la tâche goulot d’étranglement se retrouve sur l’un des cœurs lents. Cela peut avoir un effet considérable sur les temps d’image. Dans ce scénario, le temps total d’exécution du travail est de 2 250 μs, soit six fois 375 μs. Figure 3 : exemple de tâche goulot d’étranglement s’exécutant sur un cœur plus lent.



