NoGreg (le blog de LeGreg)
Retour au blog <<

[Code] GPU vs CPU : quelles différences ?

Vendredi 27 octobre 2006 à 19:42

Note d’août 08 : ce vieil article ne parle pas du GPGPU (general purpose processing on GPU) qui explose en ce moment. Ceci dit la plupart des concepts présentés sont toujours bien réels.

Questions usuelles et dont la réponse n’est pas toujours évidente :

Pourquoi le GPU est-il plus rapide pour la 3D temps réelle que le CPU ? À l’inverse pourquoi le GPU ne fait-il pas tourner (encore ?) les programmes gourmands en calcul à la place du CPU ?

Bien entendu la réponse parait simple : parce que le GPU est conçu de manière à être plus rapide pour la 3D. Mais c’est dans les détails de l’implémentation que ça devient intéressant.

Un GPU est une unité de calcul massivement parallèle. La tache qui consiste à remplir un million de pixels en moins d’une milliseconde est extrèmement répétitive, en fait on peut facilement demander à N sous unités de remplir un million/N polygones sur chacune de leur partie d’écran. Doubler la performance sur cette tache simple reviendrait donc à doubler le nombre d’unités qui remplissent les pixels.

Bien entendu il y a une limite à cette parallélisation, la première est que chaque sous-unité occupe une aire de silicium non nulle et que doubler le nombre de sous-unités revient à plus que doubler le cout du processeur final : on peut mettre deux fois moins de puces par wafer et, aussi, plus la taille du processeur est grosse moins le yield est bon, il y a une taille limite au delà de laquelle produire un processeur sera économiquement infaisable (sans parler de la dissipation thermique et d’autres problèmes d’architectures liés aux gros chips). Cette limitation est repoussée d’année en année par les gains sur les processus de manufacture des circuits intégrés, ce qui permet de rendre économiquement faisable des processeurs qui doublent leur nombre de transistors actifs d’une année sur l’autre.

La deuxième limitation est que la vitesse finale du système entier est déterminé par la loi d’Amdahl : sur un système composé de n composants, s’il est faisable de faire progresser certains des composants à une certaines vitesse, il y a des composants qui par nature ou d’autres raisons vont stagner en performance et qui donc vont déterminer la vitesse du système à eux tout seuls. Typiquement dans un jeu vidéo le CPU qui orchestre les opérations pour le GPU est un facteur limitant certain, le GPU peut s’en sortir en augmentant continuellement la charge qui lui est assignée (augmentation de la résolution, de l’antialiasing, des calculs d’éclairage etc..) mais si SA charge n’augmente pas alors le GPU peut apparaitre comme stagnant sur une application qui serait limité par le CPU. Un autre facteur limitant est la capacité des composants mémoires. Ces derniers sont reliés au GPU par des lignes dédiées qui peuvent fonctionner en parallèle, mais le nombre de ces lignes dédiées sont limités “physiquement” par l’espace qu’il y a sur la carte entre la puce principale et le composant mémoire, de plus ces unités ne peuvent fonctionner en parallèle que si la charge est équitablement répartie entre chaque puce mémoire. Multiplier les sous-unités de calcul a donc comme pendant la multiplication des unités de mémoires mais les avancées sont beaucoup moins nombreuses dans ce domaine que les avancées qui permettent de multiplier la puissance de calcul. Il devient donc intéressant de développer des algorithmes qui permettent d’outrepasser en partie ces besoins non remplis en bande passante mémoire. Les progrès de la physique du silicium ne peuvent donc pas se passer d’avancées sur les algorithmes de rendu efficaces (qui peuvent à leur tour mettre à profit la puissance de calcul dégagée).

Le GPU met également à profit une autre forme de parallélisme. La tache qui consiste à tracer un triangle à l’écran parait simple mais est incroyablement compliquées faisant intervenir des milliers de calculs de transformation géométrique, de projection, de setup, de rasterisation, de calcul d’éclairage et texturage par pixel, de tests d’occlusion et de combinaison avec le contenu d’un tampon en mémoire vidéo. De l’entrée à la sortie la vie d’un triangle à l’intérieur du GPU peut couvrir des millions de transitions d’état des transistors. Si l’on devait considérer la tache de tracé du triangle comme la tache élémentaire du GPU, alors on aurait à l’heure actuelle des GPUs avec des cycles d’horloge de l’ordre du kilohertz ou moins. La solution consiste à pipeliner le traitement. Le pipelining est l’équivalent du taylorisme en usine, chaque commande est découpée en stages, chaque stage est découpée en sous-opérations élementaires dont la plus complexe devra avoir un faible nombre de transitions puisque c’est elle déterminera la cadence ou le cycle d’horloge. Le pipelining a plusieurs avantages, l’un est que le traitement sur chaque commande peut intervenir en parallèle (c’est pour cela que le pipelining est une forme de parallélisme), l’unité 0 accepte la commande A, passe une fois fini à l’unité 1 puis accepte la commande B, etc. Il y a donc un grand nombre de commandes en cours de traitement. L’autre avantage est que comme on a divisé en plein de taches élémentaires il est possible d’appliquer le principe de multiplication des unités sur les parties du pipeline qui auront à faire les taches les plus compliquées et les plus nombreuses.

En guise d’exemple, imaginons qu’on soit à l’usine du père noel, il y a trois stages dans le pipeline, l’un accepte la commande et réunit les composants, le deuxième assemble les composants en jouet et passe au troisième qui empaquete et expédie. Si le premier peut accepter 10 commandes à la minutes, le deuxième peut assembler 1 jouet par minute et le dernier peut empaqueter 5 paquets à la minute, on voit que le maximum que l’on peut produire est 1 jouet par minute si l’on a trois personnes. Si l’on peut embaucher 4 personnes et les placer à l’assemblage de jouets alors on multiplie le rendement à 5 jouets par minute et on évite à la personne qui empaquete de se tourner les pouces 80% de son temps. Si l’on n’a que dix personnes à répartir il y aura une répartition optimale qui maximisera la vitesse de sortie des jouets (1 à la commande, 2 à l’empaquetage et le reste à l’assemblage ce qui garantira 7 jouets à la minute).

Typiquement dans la vie d’un triangle l’opération la plus couteuse est le traitement par pixel, c’est donc pour ça qu’on augmente toujours fortement le nombre d’unités de calculs par pixel. Chaque unité de calcul par pixel est elle-meme pipeliné etc..

Ce n’est pas le seul avantage qu’a un GPU sur un CPU pour les opérations sur les triangles. Comme les opérations à effectuer sont toujours les mêmes d’une application à l’autre il y a donc un intéret à dédier une partie du silicium à ces opérations spécifiques. C’est pour ça qu’il y a des unités spéciales dédiées au filtrage des textures, d’autres dédiées à la comparaison avec le ZBuffer, d’autres optimisées pour le traitement de la géométrie et la projection etc.. Le nombre de flops (instructions flottantes par seconde) représenté par ces opérations spécifiques est énorme mais la spécialisation permet de les implanter avec beaucoup moins de silicium que s’il avait fallu faire les calculs dans des unités de calculs très génériques. Le CPU n’ayant aucune de ces opérations spécifiques et constantes ne peut donc se permettre de dédier une partie important de sa surface de silicium pour accélerer de telles opérations. Il est donc contraint de tout faire avec ses unités de calcul génériques et donc à puissance de calcul égale un GPU serait donc bien plus efficace sur ces taches dédiées qu’un CPU.

Ceci dit on ne s’interesse pas trop aux flops générées par ces opérations spécifiques parce que justement elles sont figées. Mais mêmes sur les flops liées aux opérations “flexibles” (représentées sur les GPUs modernes par les pixels et vertex shaders programmables), l’avantage semble sur le papier au GPU.

Alors aucun avantage au CPU ? En fait pas si vite.

Pour toutes les taches qui sont fortement “data parallel” ou SIMD (un seul compteur d’instructions : simple instruction, données multiples), le GPU est à son avantage. De même pour toutes les tâches qui consistent à tracer des triangles puisque l’on peut mettre à profit tout la puissance de calcul dédiée. Mais si la tâche consiste à travailler sur un seul thread d’éxecution et sur une donnée à la fois, cet avantage disparait. L’argument économique existe également pour le CPU, à savoir que sa taille en transistor est déterminé par les capacités de la physique du silicium, mais la différence c’est que le CPU ne pouvant mettre à profit ni une parallélisation facile, ni un gros pipelining, doit donc utiliser ces transistors pour maximiser le nombre d’instructions unique exécutées par seconde. Il va donc dédier une grosse partie de sa surface à l’exécution “désordonnée”, à la prédiction de branchement, au register renaming. Et surtout une énorme partie va être dédiée au cache d’instruction et de données. Si une instruction dépend d’une information située en mémoire principale, le délai d’attente sera assez long, ce qui causera le CPU à rester à ne rien faire pendant une bonne partie de son temps. Pour éviter au CPU de se tourner les pouces, on va rapatrier une bonne partie de cette mémoire localement, pour permettre aux unités de calcul de travailler à leur vitesse de croisière. Plus le cache est gros plus on limite ces délais d’attente.

Une partie non négligeable des transistors est également utilisée pour améliorer le pipelining, une commande simple de GPU est plusieurs ordres de magnitude plus grand qu’une commande simple de CPU, ce dernier ne peut donc pas les traiter à la même cadence. La subdivision en instructions élémentaires est donc beaucoup plus fine ce qui lui permet d’afficher assez facilement des vitesses de l’ordre du gigahertz jusqu’à plusieurs gigahertz.

Le CPU est fortement limité par son jeu d’instructions (= les commandes du CPU), ce dernier est plus ou moins figé et n’a que des opérations très élémentaires. Les gains en efficacité sont fortement limités par ce jeu d’instruction mais en contrepartie il y gagne un gros avantage. Comme chaque commande est très simple, changer de commande en cours de route en se basant sur une commande précédente est relativement peu couteux (encore moins si l’on utilise la prédiction de branchement) et donc chaque nouvelle commande peut-etre rendue dépendante de la précédente. À l’inverse pour le GPU, si chaque commande devait être dépendante d’une précédente, le gros pipeline resterait vide la plupart du temps. L’interface de programmation 3D (API) du GPU interdit ou limite fortement la possibilité de faire des décisions de rendu basées sur une commande précédente. La prédiction de branchement au niveau du gros pipeline n’est également pas réaliste tout simplement parce que les commandes étant plus complexes, la manière dont elles pourraient s’affecter mutuellement sont trop nombreuses et donc non prévisibles. Bien entendu rien n’interdirait de faire de la prédiction de branchement au sein d’une unité de calcul élémentaire du GPU, de même il semble qu’il y ait une flexibilité importante au niveau des instructions de pixel shaders (qui se rapproche en terme de simplicité des instructions d’un CPU). Cependant si l’on a besoin de ce niveau de flexibilité et que l’on doit se limiter aux opérations faisables dans les shaders, on perd l’intéret de toutes les unités dédiées et des différents stages du gros pipeline. De plus comme les calculs sont faits de manière fortement SIMD (plusieurs milliers de données traitées par une seule instruction), les décisions à faire sont beaucoup mieux servies si elles affectent simultanément un grand nombre de ces données. La flexibilité des instructions des shaders n’approche donc celle du CPU qu’en surface.

On voit donc que les GPUs et les CPUs sont soumis aux mêmes lois physiques et économiques mais ce faisant ne font pas les mêmes choix d’allocations des transistors parce qu’ils travaillent sur des domaines de calcul pour l’instant diamétralement opposés. De plus le modèle actuel du PC est fortement asymmétrique en ce qui concerne le flux de données, le GPU se trouve toujours en aval du CPU et cela ne changera pas sans une radicale rearchitecture de nos PC et de nos systèmes d’exploitation.

LeGreg

Articles liés :
Le graphisme avant les cartes graphiques -
CPU vs GPU par Mythbuster -
Microsoft WARP, rendu sur le CPU -

par H.Reaper
27 octobre 2006 à 20:38

document très interressant.

je le rajoute à mes documents personnels tout en gardant le crédit.

félicitation.

par Anal-Breath
28 octobre 2006 à 17:34

Je t’aime!

par golan8x
29 octobre 2006 à 12:51

Tu bosses dans le milieu ?

par Rodolphe
29 octobre 2006 à 13:12

et les ppu? tu n’as pas parlé des ppu!
j’déc’

par Aleph
29 octobre 2006 à 13:14

Waouh !

J’ai relu plusieurs fois pour essayer de bien assimiler et j’ai quelques questions m’sieur:

chaque stage est découpée en sous-opérations élementaires dont la plus complexe devra avoir un faible nombre de transitions puisque c’est elle déterminera la cadence ou le cycle d’horloge.

Transitions ??? qué ???
Qu’est ce qui détermine le cycle d’horloge exactement ? Une transition ou l’ensemble des transitions qui consituent la sous opération la plus complexe ?

changer de commande en cours de route en se basant sur une commande précédente est relativement peu couteux (encore moins si l’on utilise la prédiction de branchement) et donc chaque nouvelle commande peut-etre rendue dépendante de la précédente.

En quoi est-ce un avantage ?

Voilou sinon c’était interessant,

par LeGreg
29 octobre 2006 à 20:54

Transitions ??? qué ???Qu’est ce qui détermine le cycle d’horloge exactement ? Une transition ou l’ensemble des transitions qui consituent la sous opération la plus complexe ?

En fait l’unité élémentaire d’un processeur (que ce soit un gpu ou un cpu) est le transistor, cette unité est une "bascule" qui a une entrée et des sorties et met un temps en nanosecondes (picosecondes, fentosecondes) pour basculer suivant les entrées. Comme on ne peut pas faire grand chose de complexe avec un seul transistor, il en faut plusieurs en série/parallèle pour avoir une opération élémentaire. Comme il faut des millions de ces opérations élémentaire pour faire une puce il faut donc les synchroniser ensemble, c’est pourquoi tu as une horloge globale. Cette horloge globale doit donc prendre en compte la durée de toutes les opérations élémentaires et prendre la valeur d’horloge qui permettra a chacune de ces opérations de finir avant d’avoir à commencer la suivante. En général on designe pour une certaine valeur d’horloge en tablant sur des problèmes physiques (fuites de courant, production de chaleur) qui empecheront d’atteindre cette valeur d’horloge théorique.

changer de commande en cours de route en se basant sur une commande précédente est relativement peu couteux (encore moins si l’on utilise la prédiction de branchement) et donc chaque nouvelle commande peut-etre rendue dépendante de la précédente.

Exemple de commandes simples que tu pourrais vouloir implémenter :

Trace triangle A
si triangle A visible alors trace triangle B
sinon trace triangle C.

Sur un processeur comme le CPU ou le cout de la décision (branchement à cause du "si") est faible, c’est une bonne idée d’implémenter cela de manière naive. Sur un processeur où le coût de la décision est énorme (GPU), il est préférable d’implémenter cela de manière détournée, par exemple en utilisant le rendu conditionnel (direct3d10) mais meme cette technique est (relativement) plus couteuse que si tu avais tout fait sur un CPU. Le fin mot de l’histoire c’est que si tes algos de rendus utilisent "énormément" de ces approches non naturelles pour les GPUs, tu auras aussi vite fait de les coder sur le CPU, bref passer de l’un à l’autre demande une certaine tournure d’esprit.

par Aleph
29 octobre 2006 à 21:36

Ah ok je comprends pour le cycle d’horloge maintenant !

Mais permet moi d’être lourd : dont la plus complexe devra avoir un faible nombre de transitions

Alors c’est quoi une transition ? Un battement de l’horloge ? Mais si c’est ça alors l’opération entière ne peut pas déterminer le cycle d’horloge (ou alors operation = n * battement d’horloge). Ou alors transition = transistor ?
Olala je suis perdu tout d’un coup.

Pour la seconde explication ça me parait clair (ne pas faire de branchements sur un GPU) mais (haha j’en ai pas fini) :
chaque nouvelle commande peut-etre rendue dépendante de la précédente.

Donc ça offre quel avantage ? Ou alors c’est pas ça l’avantage et j’ai encore rien compris.

Désolé de t’embêter comme ça !

par LeGreg
29 juillet 2008 à 9:36

Transition = passage du transistor d’un état à un autre. Plusieurs transistors dans un circuit, chaque transition entraine une autre transition en cascade. Quand on a atteint l’état final (multiplication finie etc..) alors on peut basculer l’horloge et faire l’opération élémentaire suivante. Ceci pour beaucoup beaucoup simplifier..

Commenter

Si vous avez un compte sur WeFrag, connectez-vous pour publier un commentaire.

Tags autorisés : <a href="" title="">...</a>,<b>...</b>,<blockquote cite="">...</blockquote>,<code>...</code>,<i>...</i>.