Comment exploiter le parallelisme entre CPU et GPU ?
Une application 3D temps réel exploite toujours le processeur central (CPU) et la carte graphique (GPU) intensivement, et de manière alternée. Etant deux ressources séparées physiquement, on peut donc clairement les faire travailler en parallèle et gagner en performances. Cependant, mettre en place ce parallelisme et l'exploiter de manière optimale n'est pas toujours évident.
Ce qui rend possible la parallélisation entre la CPU et la GPU, est le fait que les envois de triangles à la carte graphique (DrawPrimitive pour DirectX, glDrawArrays avec OpenGL par exemple) rendent la main au programme immédiatement, sans attendre que ceux-ci soient traités et affichés à l'écran. Ainsi une stratégie évidente pour paralleliser l'exécution, et de placer les traitements lourds pour la CPU juste après l'envoi de triangles à la GPU.
Mais ce n'est pas suffisant. Pour être efficace, il faut tenter d'envoyer le plus de triangles avec le moins d'appels possibles (voir "Qu'est-ce que le batching ?"). Ainsi la GPU sera occupée plus longtemps sans nécessiter d'interventions de la CPU, et cette dernière aura plus de temps devant elle. Si vous envoyez les triangles à la GPU par groupes de 100, visiblement vous ne pourrez rien paralléliser, la CPU passant plus de temps dans les appels drivers que la GPU dans le traitement des triangles.
Un autre point clef de la parallélisation, est le pipeline d'exécution de la GPU. En gros, la carte graphique est programmée pour exécuter certaines actions dans un certain ordre, et tant que ceci est respecté alors vous en tirerez les meilleurs performances (voir notamment "Comment est organisé le pipeline 3D ?"). Si par contre vous tentez d'interrompre cette organisation par un appel inapproprié, vous risquez de casser le flot des instructions et de resynchroniser CPU et GPU ; vous perdez donc instantanément tout parallélisme. Plus concrètement, si vous tentez par exemple de récupérer le contenu du back-buffer, la carte graphique sera obligée de stopper ce qu'elle était en train de faire (on parle de flush du pipeline), et de se resynchroniser avec la CPU pour lui transmettre les données souhaitées. Ainsi si vous souhaitez effectuer ce genre d'appels, faites le de préférence lorsque vous êtes certain que la GPU ne fait rien, par exemple entre l'affichage à l'écran et le début du rendu.
Pour résumer, voici un schéma d'un programme exploitant très mal la parallelisation :
Ici on voit bien que beaucoup de temps est gaspillé : lorsque la CPU travaille la GPU ne fait rien, et vice-versa.
Et en voici un qui exploite au mieux celle-ci :
Ici les deux processeurs tournent en même temps, permettant de diviser énormément le temps d'exécution d'une frame.
Ceci est bien sûr une caricature, dans un moteur complexe on aura plusieurs envois de batchs alternés, l'important étant de toujours placer une utilisation de la CPU pendant que la GPU les traite. Si on a un rendu multi-passes, on peut par exemple calculer une passe pendant que la GPU rend la passe précédente, plutôt que de tout calculer en bloc avant le rendu.
Partager