Avant de rentrer dans les détails de la science informatique, un petit rappel sur ce qu’est le multithreading. Il s’agit de la capacité d’un programme à effectuer des calculs en parallèle sur les différents cœurs (core en anglais) du processeur. Quand l’idée est apparue dans les années 2000, tout le monde a trouvé ça merveilleux. Y compris les joueurs de jeu vidéo. Imaginez plutôt : il n’y aurait plus une seule unité de calcul dans le CPU, mais deux, quatre, six, voire huit, capables de cravacher simultanément. À l’époque, on nous expliquait qu’un cœur s’occuperait de la logique du jeu, un autre ferait tourner les algorithmes d’intelligence artificielle, un troisième gérerait le son, un quatrième le pathfinding, etc. Et puis... ça ne s’est pas du tout passé comme ça. Presque deux décennies après la démocratisation des processeurs multi-cœurs, les développeurs de jeu vidéo peinent encore à les exploiter. Prenez Factorio, Skyrim, Rimworld, Dwarf Fortress, Kerbal Space Program, Prison Architect, Banished : ces grands jeux n’utilisent qu’un seul cœur, et c’est exactement pour cela que vous les verrez ramer péniblement dans certaines situations. Leurs créateurs seraient-il d’énormes feignasses incapables de coder correctement ? Pas vraiment. Car le multithreading est en fait une épouvantable prise de tête.
Transformé en archive gratuite
- Cet article, initialement réservé aux abonnés, est devenu gratuit avec le temps.
Les méandres du multithreading
Ou pourquoi les jeux n'utilisent qu'une fraction de votre CPU à 300 euros
Parcourez n’importe quel forum dédié à un jeu vidéo populaire, et vous finirez pas trouver un inévitable fil de discussion sur le multithreading. « J’ai regardé sur le gestionnaire des tâches Windows, et Turbo Groumpf Fighterz X n’utilise qu’un seul cœur de mon processeur ! Pourquoi ne pas exploiter toute la puissance disponible ??? » La réponse est complexe, nous allons essayer de la résumer de manière claire et concise.
Je ne réponds plus de mon core. Pour comprendre le problème, il faut évoquer un concept essentiel en informatique : le déterminisme. Jusqu’à l’apparition du multithreading, les ordinateurs étaient des machines parfaitement déterministes. Exemple le plus simple : vous demandez au PC de calculer 2 + 2, il vous répondra toujours, invariablement, 4. Maintenant, demandons-lui d’effectuer deux calculs sur un unique cœur (c’est-à-dire sur un thread unique). En entrant 2 + 2 puis 3 + 3, il répondra toujours 4, puis 6. Si l’on fait la même chose en multithreading, les choses se compliquent. Un des cœurs du CPU fait l’opération 2 + 2. En même temps, un autre calcule 3 + 3. Que va alors afficher le PC ? Il est impossible de répondre avec certitude à la question. Il pourra afficher 4, puis 6. Mais aussi 6, puis 4. Le programmeur qui a demandé ces calculs n’aura aucun moyen de savoir quel résultat va tomber en premier. Et ce petit détail, lors du développement d’un jeu vidéo où des milliers de calculs sont effectués en permanence en temps réel, n’est pas anodin.
La course au repas. Je vous donne un exemple plus concret. Imaginez un Rimworld fictif qui aurait été « multithreadé », de manière à ce que chaque petit personnage fasse ses calculs d’IA sur un cœur CPU différent. Chantal, gérée par le cœur 1, cherche à manger. Des algorithmes balayent ses environs et trouvent un item « nourriture » sur une case. Mais Raymond, sur le cœur 2, fait exactement la même chose, en même temps, et trouve le même item. Au final, quel personnage va manger ? Impossible à dire. Et si vous rechargez une sauvegarde effectuée juste avant ces calculs, le déroulé futur du jeu ne sera pas forcément le même. Point bonus : si les deux personnages se dirigent vers le même item et cherchent à l’accaparer en même temps, vous pouvez être certain que tout va planter si le programmeur n’a pas prévu cette situation – techniquement, ça s’appelle une racing condition. À vouloir faire le calcul en parallèle (en multithreading) plutôt que séquentiellement, on introduit donc de nouveaux degrés de complexité dans la programmation de la logique du jeu. Notez que l’exemple n’est pas de moi, c’est celui qu’a donné Tynan Sylvester, le créateur de Rimworld, en réponse à une question sur le forum officiel du jeu.
Compliqué et pas forcément utile. Alors bien sûr, les développeurs peuvent prendre en compte le non-déterminisme du multithreading. Mais la tâche est loin d’être simple. Si le multithreading n’a pas été pris en compte dès l’écriture des premières lignes de code, il est difficile d’en rajouter au forceps une fois le jeu terminé à 80 %. Et surtout, c’est très souvent impossible. Faire des calculs en parallèle, c’est bien joli, mais quel intérêt si le calcul no 2 a absolument besoin du calcul no 1 pour démarrer ? Il faut aussi noter que le multithreading peut conduire à une diminution des performances, si les gains qu’il apporte ne compensent pas les ressources supplémentaires (création de zones mémoire particulières, copies d’objets entre différentes tâches...) exigées. Cerise sur le gâteau, les opérations en multithreading sont aussi bien plus difficiles à débuguer que des opérations séquentielles classiques.
Core avec les doigts. Pour ne pas terminer sur une note négative, signalons quand même que les choses s’améliorent. Petit à petit, les développeurs apprennent à dompter le multithreading. Les moteurs propriétaires des gros éditeurs (Ubisoft, EA, Activision...) l’exploitent de mieux en mieux. Dans le domaine des simulateurs, où le CPU est souvent le facteur limitant, on a vu ces dernières années des acteurs importants (comme Eagle Dynamics pour DCS World ou Laminar Research avec X-Plane) opérer des refontes complètes de leur moteur pour mieux exploiter les cœurs multiples des processeurs modernes. Et Unity, qui fait probablement tourner 75 % des titres de votre bibliothèque Steam et n’a jamais officiellement supporté le multithreading, est justement en train de l’implémenter, avec une nouvelle fonction de programmation, les jobs. Un Rimworld 2 avec plusieurs centaines de colons gambadant sur la carte sans le moindre ralentissement pourrait bientôt devenir tout à fait réalisable.