Programmation pour le multicœur sur Xbox 360 et Windows
Pendant des années, les performances des processeurs ont augmenté régulièrement, et les jeux et autres programmes ont récolté les avantages de cette puissance croissante sans avoir à faire quoi que ce soit de spécial.
Les règles ont changé. Les performances des cœurs monoprocesseurs augmentent maintenant très lentement, voire pas du tout. Toutefois, la puissance de calcul disponible dans un ordinateur ou une console classique continue de croître. La différence est que la plupart de ce gain de performances provient maintenant d’avoir plusieurs cœurs de processeur dans une seule machine, souvent dans une seule puce. Le processeur Xbox 360 a trois cœurs de processeur sur une seule puce, et environ 70 % des processeurs PC vendus en 2006 étaient multicœurs.
L’augmentation de la puissance de traitement disponible est tout aussi spectaculaire que par le passé, mais les développeurs doivent maintenant écrire du code multithread pour pouvoir utiliser cette puissance. La programmation multithread apporte de nouveaux défis en matière de conception et de programmation. Cette rubrique fournit des conseils sur la façon de démarrer avec la programmation multithread.
L’importance d’une bonne conception
Une bonne conception de programme multithread est essentielle, mais elle peut être très difficile. Si vous déplacez au hasard vos principaux systèmes de jeu sur différents threads, vous constaterez probablement que chaque thread passe la plupart de son temps à attendre les autres threads. Ce type de conception entraîne une complexité accrue et un effort de débogage important, avec pratiquement aucun gain de performances.
Chaque fois que les threads doivent synchroniser ou partager des données, il existe un risque d’altération des données, de surcharge de synchronisation, d’interblocages et de complexité. Par conséquent, votre conception multithread doit documenter clairement chaque point de synchronisation et de communication, et elle doit réduire ces points autant que possible. Lorsque les threads doivent communiquer, l’effort de codage augmente, ce qui peut réduire la productivité s’il affecte trop de code source.
L’objectif de conception le plus simple pour le multithreading est de diviser le code en grandes parties indépendantes. Si vous limitez ensuite ces éléments à la communication seulement quelques fois par image, vous constaterez une accélération significative de la multithreading, sans complexité excessive.
Tâches threadées typiques
Quelques types de tâches se sont avérés utilisables pour être placées sur des threads distincts. La liste suivante n’est pas destinée à être exhaustive, mais devrait donner quelques idées.
Rendu
Le rendu, qui peut inclure la marche à pied du graphe de scène ou, éventuellement, l’appel de fonctions D3D uniquement, représente souvent 50 % ou plus du temps processeur. Par conséquent, le déplacement du rendu vers un autre thread peut avoir des avantages significatifs. Le thread de mise à jour peut remplir une sorte de mémoire tampon de description du rendu, que le thread de rendu peut ensuite traiter.
Le thread de mise à jour du jeu a toujours une image avant le thread de rendu, ce qui signifie qu’il faut deux images avant que les actions de l’utilisateur s’affichent à l’écran. Bien que cette latence accrue puisse être un problème, la fréquence d’images accrue du fractionnement de la charge de travail maintient généralement la latence totale acceptable.
Dans la plupart des cas, tout le rendu est toujours effectué sur un seul thread, mais il s’agit d’un thread différent de la mise à jour du jeu.
L’indicateur D3DCREATE_MULTITHREADED est parfois utilisé pour autoriser le rendu sur un thread et la création de ressources sur d’autres threads ; cet indicateur est ignoré sur Xbox 360 et vous devez éviter de l’utiliser sur Windows. Sur Windows, la spécification de cet indicateur force D3D à consacrer beaucoup de temps à la synchronisation, ce qui ralentit le thread de rendu.
Décompression de fichiers
Les temps de chargement sont toujours trop longs et la diffusion de données en mémoire sans affecter la fréquence d’images peut être difficile. Si toutes les données sont fortement compressées sur un disque, la vitesse de transfert des données à partir du disque dur ou du disque optique est moins susceptible d’être un facteur limitant. Sur un processeur monothread, il n’y a généralement pas suffisamment de temps processeur disponible pour la compression pour faciliter les temps de chargement. Sur un système multiprocesseur, toutefois, la décompression de fichiers utilise des cycles d’UC qui seraient autrement gaspiller ; il améliore les temps de chargement et la diffusion en continu ; et il économise de l’espace sur le disque.
N’utilisez pas la décompression de fichiers en remplacement du traitement qui doit être effectué pendant la production. Par instance, si vous consacrez un thread supplémentaire à l’analyse des données XML pendant le chargement de niveau, vous n’utilisez pas le multithreading pour améliorer l’expérience du joueur.
Lorsque vous utilisez un thread de décompression de fichier, vous devez toujours utiliser des E/S de fichier asynchrones et des lectures volumineuses afin d’optimiser l’efficacité de la lecture des données.
Graphiques Fluff
Il existe de nombreuses subtilités graphiques qui améliorent l’apparence du jeu, mais ne sont pas strictement nécessaires. Il s’agit notamment d’animations cloud générées de manière procédurale, de simulations de tissus et de cheveux, d’ondes procédurales, de végétation procédurale, de particules supplémentaires ou de physique non-jeu.
Étant donné que ces effets n’affectent pas le jeu, ils ne provoquent pas de problèmes de synchronisation difficiles: ils peuvent se synchroniser avec les autres threads une fois par image ou moins souvent. En outre, sur les jeux pour Windows, ces effets peuvent ajouter de la valeur pour les joueurs avec des processeurs multicœurs, tout en étant omis silencieusement sur les ordinateurs monocœurs, donnant ainsi un moyen facile de mettre à l’échelle sur un large éventail de fonctionnalités.
Physique
La physique ne peut souvent pas être placée sur un thread distinct pour s’exécuter en parallèle avec la mise à jour du jeu, car la mise à jour du jeu nécessite généralement les résultats des calculs physiques immédiatement. L’alternative pour la physique multithreading consiste à l’exécuter sur plusieurs processeurs. Bien que cela puisse être effectué, il s’agit d’une tâche complexe nécessitant un accès fréquent aux structures de données partagées. Si vous pouvez maintenir votre charge de travail physique suffisamment faible pour tenir sur le thread main, votre travail sera plus simple.
Les bibliothèques qui prennent en charge l’exécution de la physique sur plusieurs threads sont disponibles. Toutefois, cela peut entraîner un problème : lorsque votre jeu exécute la physique, il utilise de nombreux threads, mais le reste du temps, il en utilise peu. L’exécution de la physique sur plusieurs threads nécessite de traiter ce problème afin que la charge de travail soit répartie uniformément sur le cadre. Si vous écrivez un moteur physique multithread, vous devez porter une attention particulière à toutes vos structures de données, points de synchronisation et équilibrage de charge.
Exemples de conceptions multithread
Les jeux pour Windows doivent s’exécuter sur des ordinateurs avec différents nombres de cœurs d’UC. La plupart des machines de jeu n’ont encore qu’un seul cœur, bien que le nombre de machines à deux cœurs augmente rapidement. Un jeu typique pour Windows peut diviser sa charge de travail en un seul thread pour la mise à jour et le rendu, avec des threads de travail facultatifs pour ajouter des fonctionnalités supplémentaires. En outre, certains threads d’arrière-plan pour effectuer des E/S de fichiers et la mise en réseau seront probablement utilisés. La figure 1 montre les threads, ainsi que les points de transfert de données main.
Figure 1. Conception de threads dans un jeu pour Windows
Un jeu Xbox 360 classique peut utiliser des threads logiciels supplémentaires gourmands en processeur, de sorte qu’il peut diviser sa charge de travail en un thread de mise à jour, un thread de rendu et trois threads de travail, comme illustré dans la figure 2.
Figure 2 : Conception de threads dans un jeu pour Xbox 360
À l’exception des E/S de fichiers et de la mise en réseau, ces tâches peuvent toutes être suffisamment gourmandes en ressources processeur pour tirer parti de leur propre thread matériel. Ces tâches ont également le potentiel d’être suffisamment indépendantes pour pouvoir s’exécuter pour un cadre entier sans communiquer.
Le thread de mise à jour du jeu gère l’entrée du contrôleur, l’IA et la physique, et prépare des instructions pour les quatre autres threads. Ces instructions étant placées dans des mémoires tampons appartenant au thread de mise à jour du jeu, aucune synchronisation n’est requise car les instructions sont générées.
À la fin de l’image, le thread de mise à jour du jeu remet les mémoires tampons d’instruction aux quatre autres threads, puis commence à travailler sur l’image suivante, en remplissant un autre ensemble de mémoires tampons d’instructions.
Étant donné que les threads de mise à jour et de rendu fonctionnent en lockstep les uns avec les autres, leurs mémoires tampons de communication sont simplement doublement mises en mémoire tampon : à tout moment, le thread de mise à jour remplit une mémoire tampon tandis que le thread de rendu lit l’autre.
Les autres threads de travail ne sont pas nécessairement liés à la fréquence d’images. La décompression d’un élément de données peut prendre beaucoup moins qu’une trame, ou prendre de nombreuses images. Même la simulation de tissu et de cheveux peut ne pas avoir besoin de s’exécuter exactement à la fréquence d’images, car des mises à jour moins fréquentes peuvent être tout à fait acceptables. Par conséquent, ces trois threads ont besoin de structures de données différentes pour communiquer avec le thread de mise à jour et le thread de rendu. Ils ont chacun besoin d’une file d’attente d’entrée pouvant contenir des demandes de travail, et le thread de rendu a besoin d’une file d’attente de données pouvant contenir les résultats produits par les threads. À la fin de chaque trame, le thread de mise à jour ajoute un bloc de demandes de travail aux files d’attente des threads de travail. L’ajout d’une seule fois à la liste par image garantit que le thread de mise à jour réduit la surcharge de synchronisation. Chacun des threads de travail extrait les affectations de la file d’attente de travail aussi rapidement qu’il le peut, à l’aide d’une boucle qui ressemble à ceci :
for(;;)
{
while( WorkQueueNotEmpty() )
{
RemoveWorkItemFromWorkQueue();
ProcessWorkItem();
PutResultInDataQueue();
}
WaitForSingleObject( hWorkSemaphore );
}
Étant donné que les données vont des threads de mise à jour aux threads de travail, puis au thread de rendu, il peut y avoir un délai de trois images ou plus avant que certaines actions n’arrivent à l’écran. Toutefois, si vous affectez des tâches tolérantes à la latence aux threads de travail, cela ne devrait pas poser de problème.
Une autre conception serait d’avoir plusieurs threads de travail dessinant tous à partir de la même file d’attente de travail. Cela permettrait un équilibrage de charge automatique et rendrait plus probable que tous les threads worker restent occupés.
Le thread de mise à jour du jeu doit veiller à ne pas donner trop de travail aux threads de travail, sinon les files d’attente de travail peuvent augmenter continuellement. La façon dont le thread de mise à jour gère cela dépend du type de tâches effectuées par les threads de travail.
Multithreading simultané et nombre de threads
Tous les threads ne sont pas créés égaux. Deux threads matériels peuvent se trouver sur des puces distinctes, sur la même puce ou même sur le même cœur. La configuration la plus importante à connaître pour les programmeurs de jeux est deux threads matériels sur un seul cœur : multithreading simultané (SMT) ou technologie Hyper-Threading (technologie HT).
Les threads de technologie SMT ou HT partagent les ressources du cœur du processeur. Étant donné qu’ils partagent les unités d’exécution, l’accélération maximale de l’exécution de deux threads au lieu d’un est généralement de 10 à 20 %, au lieu de 100 % qui est possible à partir de deux threads matériels indépendants.
Plus important encore, les threads SMT ou HT Technology partagent les caches d’instructions et de données L1. Si leurs modèles d’accès à la mémoire sont incompatibles, ils peuvent finir par se battre sur le cache et provoquer de nombreuses erreurs de cache. Dans le pire des cas, les performances totales du cœur de processeur peuvent effectivement diminuer lorsqu’un deuxième thread est exécuté. Sur Xbox 360, il s’agit d’un problème assez simple. La configuration de la Xbox 360 est connue (trois cœurs d’uc chacun avec deux threads matériels), et les développeurs attribuent leurs threads logiciels à des threads d’UC spécifiques et peuvent mesurer pour voir si leur conception de threading leur offre des performances supplémentaires.
Sur Windows, la situation est plus compliquée. Le nombre de threads et leur configuration varient d’un ordinateur à l’autre, et la détermination de la configuration est compliquée. La fonction GetLogicalProcessorInformation fournit des informations sur la relation entre différents threads matériels, et cette fonction est disponible sur Windows Vista, Windows 7 et Windows XP SP3. Par conséquent, pour l’instant, vous devez utiliser l’instruction CPUID et les algorithmes donnés par Intel et AMD afin de déterminer le nombre de threads « réels » disponibles. Pour plus d’informations, consultez les références.
L’exemple CoreDetection dans le Kit de développement logiciel (SDK) DirectX contient un exemple de code qui utilise la fonction GetLogicalProcessorInformation ou l’instruction CPUID pour retourner la topologie de base du processeur. L’instruction CPUID est utilisée si GetLogicalProcessorInformation n’est pas pris en charge sur la plateforme actuelle. CoreDetection se trouve aux emplacements suivants :
-
Source:
-
Sdk DirectX root\Samples\C++\Misc\CoreDetection
-
Exécutable:
-
Kit de développement logiciel (SDK) DirectX root\Samples\C++\Misc\Bin\CoreDetection.exe
L’hypothèse la plus sûre est de ne pas avoir plus d’un thread gourmand en processeur par cœur de processeur. Le fait d’avoir plus de threads gourmands en ressources processeur que de cœurs de processeur offre peu ou pas d’avantages, et apporte la surcharge et la complexité supplémentaires de threads supplémentaires.
Création de threads
La création de threads est une opération assez simple, mais il existe de nombreuses erreurs potentielles. Le code ci-dessous montre la façon appropriée de créer un thread, d’attendre qu’il se termine, puis de le nettoyer.
const int stackSize = 65536;
HANDLE hThread = (HANDLE)_beginthreadex( 0, stackSize,
ThreadFunction, 0, 0, 0 );
// Do work on main thread here.
// Wait for child thread to complete
WaitForSingleObject( hThread, INFINITE );
CloseHandle( hThread );
...
unsigned __stdcall ThreadFunction( void* data )
{
#if _XBOX_VER >= 200
// On Xbox 360 you must explicitly assign
// software threads to hardware threads.
XSetThreadProcessor( GetCurrentThread(), 2 );
#endif
// Do child thread work here.
return 0;
}
Lorsque vous créez un thread, vous avez la possibilité de spécifier la taille de la pile pour le thread enfant ou de spécifier zéro, auquel cas le thread enfant héritera de la taille de la pile du thread parent. Sur Xbox 360, où les piles sont entièrement validées au démarrage du thread, la spécification de zéro peut gaspiller beaucoup de mémoire, car de nombreux threads enfants n’auront pas besoin d’autant de pile que le parent. Sur Xbox 360, il est également important que la taille de la pile soit un multiple de 64 Ko.
Si vous utilisez la fonction CreateThread pour créer des threads, le runtime C/C++ (CRT) n’est pas correctement initialisé sur Windows. Nous vous recommandons d’utiliser la fonction _beginthreadex CRT à la place.
La valeur de retour de CreateThread ou _beginthreadex est un handle de thread. Ce thread peut être utilisé pour attendre que le thread enfant se termine, ce qui est beaucoup plus simple et beaucoup plus efficace que de tourner dans une boucle vérifiant le thread status. Pour attendre que le thread se termine, appelez simplement WaitForSingleObject avec le handle de thread.
Les ressources du thread ne sont pas libérées tant que le thread n’est pas terminé et que le descripteur de thread n’a pas été fermé. Par conséquent, il est important de fermer le handle de thread avec CloseHandle lorsque vous en avez terminé. Si vous attendez que le thread se termine avec WaitForSingleObject, veillez à ne pas fermer le handle tant que l’attente n’est pas terminée.
Sur Xbox 360, vous devez affecter explicitement des threads logiciels à un thread matériel particulier à l’aide de XSetThreadProcessor. Sinon, tous les threads enfants restent sur le même thread matériel que le parent. Sur Windows, vous pouvez utiliser SetThreadAffinityMask pour suggérer fortement au système d’exploitation les threads matériels sur lesquels votre thread doit s’exécuter. Cette technique doit généralement être évitée sur Windows, car vous ne savez pas quels autres processus peuvent être en cours d’exécution sur le système. Il est généralement préférable de laisser le planificateur Windows affecter vos threads à des threads matériels inactifs.
La création de threads est une opération coûteuse. Les threads doivent être créés et détruits rarement. Si vous souhaitez créer et détruire fréquemment des threads, utilisez un pool de threads qui attendent le travail à la place.
Synchronisation des threads
Pour que plusieurs threads fonctionnent ensemble, vous devez être en mesure de synchroniser les threads, de passer des messages et de demander un accès exclusif aux ressources. Windows et Xbox 360 sont fournis avec un ensemble complet de primitives de synchronisation. Pour plus d’informations sur ces primitives de synchronisation, consultez la documentation de la plateforme.
Accès exclusif
L’obtention d’un accès exclusif à une ressource, à une structure de données ou à un chemin de code est un besoin courant. L’une des options permettant d’obtenir un accès exclusif est un mutex, dont l’utilisation typique est illustrée ici.
// Initialize
HANDLE mutex = CreateMutex( 0, FALSE, 0 );
// Use
void ManipulateSharedData()
{
WaitForSingleObject( mutex, INFINITE );
// Manipulate stuff...
ReleaseMutex( mutex );
}
// Destroy
CloseHandle( mutex );
The kernel guarantees that, for a particular mutex, only one thread at a time can
acquire it.
The main disadvantage to mutexes is that they are relatively expensive to acquire
and release. A faster alternative is a critical section.
// Initialize
CRITICAL_SECTION cs;
InitializeCriticalSection( &cs );
// Use
void ManipulateSharedData()
{
EnterCriticalSection( &cs );
// Manipulate stuff...
LeaveCriticalSection( &cs );
}
// Destroy
DeleteCriticalSection( &cs );
Les sections critiques ont une sémantique similaire aux mutex, mais elles peuvent être utilisées pour se synchroniser uniquement au sein d’un processus, et non entre les processus. Leur avantage main est qu’ils s’exécutent environ vingt fois plus vite que les mutex.
Événements
Si deux threads (peut-être un thread de mise à jour et un thread de rendu) utilisent à tour de rôle une paire de mémoires tampons de description de rendu, ils ont besoin d’un moyen d’indiquer quand ils ont terminé avec leur mémoire tampon particulière. Pour ce faire, associez un événement (alloué avec CreateEvent) à chaque mémoire tampon. Lorsqu’un thread est terminé avec une mémoire tampon, il peut utiliser SetEvent pour signaler cela, puis appeler WaitForSingleObject sur l’événement de l’autre mémoire tampon. Cette technique extrapole facilement pour tripler la mise en mémoire tampon des ressources.
Sémaphores
Un sémaphore est utilisé pour contrôler le nombre de threads pouvant être en cours d’exécution et est couramment utilisé pour implémenter des files d’attente de travail. Un thread ajoute du travail à une file d’attente et utilise ReleaseSemaphore chaque fois qu’il ajoute un nouvel élément à la file d’attente. Cela permet à un thread de travail d’être libéré du pool de threads en attente. Les threads de travail appellent simplement WaitForSingleObject et, lorsqu’il est retourné, ils savent qu’il y a un élément de travail dans la file d’attente pour eux. En outre, une section critique ou une autre technique de synchronisation doit être utilisée pour garantir un accès sécurisé à la file d’attente de travail partagée.
Éviter SuspendThread
Parfois, lorsque vous souhaitez qu’un thread arrête ce qu’il fait, il est tentant d’utiliser SuspendThread au lieu des primitives de synchronisation correctes. C’est toujours une mauvaise idée et peut facilement conduire à des interblocages et d’autres problèmes. SuspendThread interagit également mal avec le débogueur Visual Studio. Évitez SuspendThread. Utilisez WaitForSingleObject à la place.
WaitForSingleObject et WaitForMultipleObjects
La fonction WaitForSingleObject est la fonction de synchronisation la plus couramment utilisée. Toutefois, vous souhaitez parfois qu’un thread attende que plusieurs conditions soient remplies simultanément ou que l’une d’un ensemble de conditions soit remplie. Dans ce cas, vous devez utiliser WaitForMultipleObjects.
Fonctions verrouillées et programmation sans verrou
Il existe une famille de fonctions permettant d’effectuer des opérations thread-safe simples sans utiliser de verrous. Il s’agit de la famille interblocée de fonctions, telles que InterlockedIncrement. Ces fonctions, ainsi que d’autres techniques utilisant un réglage minutieux des indicateurs, sont ensemble appelées programmation sans verrou. La programmation sans verrouillage peut être extrêmement difficile à faire correctement, et est beaucoup plus difficile sur Xbox 360 que sur Windows.
Pour plus d’informations sur la programmation sans verrous, consultez Considérations relatives à la programmation sans verrou pour Xbox 360 et Microsoft Windows.
Réduction de la synchronisation
Certaines méthodes de synchronisation sont plus rapides que d’autres. Toutefois, plutôt que d’optimiser votre code en choisissant les techniques de synchronisation les plus rapides possibles, il est généralement préférable de synchroniser moins souvent. C’est plus rapide que la synchronisation trop fréquente, et cela rend le code plus simple et plus facile à déboguer.
Certaines opérations, telles que l’allocation de mémoire, peuvent devoir utiliser des primitives de synchronisation pour fonctionner correctement. Par conséquent, l’exécution d’allocations fréquentes à partir du tas partagé par défaut entraîne une synchronisation fréquente, ce qui gaspille certaines performances. Éviter les allocations fréquentes ou utiliser des tas par thread (à l’aide de HEAP_NO_SERIALIZE si vous utilisez HeapCreate) peut éviter cette synchronisation masquée.
Une autre cause de synchronisation masquée est D3DCREATE_MULTITHREADED, ce qui amène D3D sur Windows à utiliser la synchronisation sur de nombreuses opérations. (L’indicateur est ignoré sur Xbox 360.)
Les données par thread, également appelées stockage local de thread, peuvent être un moyen important d’éviter la synchronisation. Visual C++ vous permet de déclarer des variables globales comme étant par thread avec la syntaxe __declspec(thread).
__declspec( thread ) int tls_i = 1;
Cela donne à chaque thread dans le processus sa propre copie de tls_i, qui peut être référencée en toute sécurité et efficacement sans nécessiter de synchronisation.
La technique __declspec(thread) ne fonctionne pas avec les DLL chargées dynamiquement. Si vous utilisez des DLL chargées dynamiquement, vous devez utiliser la famille de fonctions TLSAlloc pour implémenter le stockage local des threads.
Destruction de threads
Le seul moyen sûr de détruire un thread consiste à faire quitter le thread lui-même, soit en retournant à partir de la fonction de thread main, soit en appelant le thread ExitThread ou _endthreadex. Si un thread est créé avec _beginthreadex, il doit utiliser _endthreadex ou retourner à partir de la fonction de thread main, car l’utilisation de ExitThread ne libère pas correctement les ressources CRT. N’appelez jamais la fonction TerminateThread , car le thread ne sera pas correctement nettoyé. Les threads devraient toujours se suicider , ils ne devraient jamais être assassinés.
OpenMP
OpenMP est une extension de langage permettant d’ajouter le multithreading à votre programme à l’aide de pragmas pour guider le compilateur dans la parallélisation des boucles. OpenMP est pris en charge par Visual C++ 2005 sur Windows et Xbox 360 et peut être utilisé conjointement avec la gestion manuelle des threads. OpenMP peut être un moyen pratique de multithread parties de votre code, mais il est peu probable qu’il soit la solution idéale, en particulier pour les jeux. OpenMP peut être plus applicable aux tâches de production plus longues, telles que le traitement de l’art et d’autres ressources. Pour plus d’informations, consultez la documentation visual C++ ou accédez au site web OpenMP.
Profilage
Le profilage multithread est important. Il est facile de se retrouver avec de longs blocages où les threads s’attendent les uns sur les autres. Ces décrochages peuvent être difficiles à trouver et à diagnostiquer. Pour aider à les identifier, envisagez d’ajouter l’instrumentation à vos appels de synchronisation. Un profileur d’échantillonnage peut également aider à identifier ces problèmes, car il peut enregistrer des informations de minutage sans les modifier considérablement.
Minutage
L’instruction rdtsc est un moyen d’obtenir des informations de minutage précises sur Windows. Malheureusement, rdtsc a plusieurs problèmes qui en font un mauvais choix pour votre titre d’expédition. Les compteurs rdtsc ne sont pas nécessairement synchronisés entre les processeurs. Par conséquent, lorsque votre thread se déplace entre des threads matériels, vous pouvez obtenir de grandes différences positives ou négatives. Selon les paramètres de gestion de l’alimentation, la fréquence à laquelle le compteur rdtsc s’incrémente peut également changer à mesure que votre jeu s’exécute. Pour éviter ces difficultés, vous devez préférer QueryPerformanceCounter et QueryPerformanceFrequency pour le minutage haute précision dans votre jeu d’expédition. Pour plus d’informations sur le minutage, consultez Minutage des jeux et processeurs multicœurs.
Débogage
Visual Studio prend entièrement en charge le débogage multithread pour Windows et Xbox 360. La fenêtre threads Visual Studio vous permet de basculer entre les threads afin de voir les différentes piles d’appels et variables locales. La fenêtre threads vous permet également de figer et de décongeler des threads particuliers.
Sur Xbox 360, vous pouvez utiliser la méta-variable @hwthread dans la fenêtre watch pour afficher le thread matériel sur lequel le thread logiciel actuellement sélectionné s’exécute.
La fenêtre threads est plus facile à utiliser si vous nommez vos threads de manière explicite. Visual Studio et d’autres débogueurs Microsoft vous permettent de nommer vos threads. Implémentez la fonction SetThreadName suivante et appelez-la à partir de chaque thread au démarrage.
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // must be 0x1000
LPCSTR szName; // pointer to name (in user address space)
DWORD dwThreadID; // thread ID (-1 = caller thread)
DWORD dwFlags; // reserved for future use, must be zero
} THREADNAME_INFO;
void SetThreadName( DWORD dwThreadID, LPCSTR szThreadName )
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = szThreadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException( 0x406D1388, 0,
sizeof(info) / sizeof(DWORD),
(DWORD*)&info );
}
__except( EXCEPTION_CONTINUE_EXECUTION ) {
}
}
// Example usage:
SetThreadName(-1, "Main thread");
Le débogueur de noyau (KD) et WinDBG prennent également en charge le débogage multithread.
Test
La programmation multithread peut être difficile, et certains bogues multithreads n’apparaissent que rarement, ce qui les rend difficiles à trouver et à corriger. L’une des meilleures façons de les vider consiste à les tester sur un large éventail d’ordinateurs, en particulier ceux avec quatre processeurs ou plus. Le code multithread qui fonctionne parfaitement sur un ordinateur monothread peut échouer instantanément sur un ordinateur à quatre processeurs. Les caractéristiques de performances et de minutage des processeurs AMD et Intel peuvent varier considérablement. Veillez donc à effectuer des tests sur des ordinateurs multiprocesseurs basés sur les processeurs des deux fournisseurs.
Améliorations apportées à Windows Vista et Windows 7
Pour les jeux ciblant les versions plus récentes de Windows, il existe un certain nombre d’API qui peuvent simplifier la création d’applications multithread évolutives. Cela est particulièrement vrai avec la nouvelle API ThreadPool et quelques primitives de synchronisation supplémentaires (variables de condition, verrou de lecture/écriture mince et initialisation unique). Vous trouverez une vue d’ensemble de ces technologies dans les articles suivants de MSDN Magazine :
- Améliorer la scalabilité avec les nouvelles API de pool de threads
- Primitives de synchronisation nouvelles vers Windows Vista
Les applications utilisant des fonctionnalités Direct3D 11 sur ces systèmes d’exploitation peuvent également tirer parti de la nouvelle conception pour la création simultanée d’objets et des listes de commandes de contexte différées pour une meilleure scalabilité pour le rendu multithread.
Récapitulatif
Grâce à une conception soignée qui réduit les interactions entre les threads, vous pouvez obtenir des gains de performances substantiels grâce à la programmation multithread sans ajouter de complexité excessive à votre code. Cela permettra à votre code de jeu de surfer sur la prochaine vague d’améliorations du processeur et d’offrir des expériences de jeu toujours plus attrayantes.
Références
- Jim Beveridge & Robert Weiner, Multithreading Applications in Win32, Addison-Wesley, 1997
- Chuck Walbourn, Game Timing and Multicore Processors, Microsoft Corporation, 2005
- MSDN Library : GetLogicalProcessorInformation
- OpenMP