[C#] Optimisation du code

Je viens de finir d’implémenter un algorithme à base de Roam (Realtime Optimized Adaptative Mesh) pour gérer du LOD sur une planète. Pour cela, je me suis basé sur l’article de O’Neill (rien a voir avec stargate) sur gamasutra.com, et ai convertit du code C++/Ogl vers C#/DirectX.

Toutefois j’ai un gros problème de vitesse sur mon algorithme: sur une bête de course, j’ai du mal a afficher plus de 5000 polys. Mon outil de profiling m’indique que ce sont les fonctions de splitting et merging qui bouffe 90% du temps CPU (environ, j’ai plus les chiffres exacts).

Ces fonctions sont appellés souvent, et usent et abusent des références entre objets (par exemple un triangle possède une référence sur chaque voisin, le quad dont il fait partie, le quad parent, et j’en passe et des meilleures). J’ai aussi beaucoup de getter et setters (pure) pour mes objets triangles.

Ma question est: comment optimiser cela pour que cela aille plus vite? J’ai pensé à tester en virant les pure getter/setter (mon profiler indique aucune amélioration/amélioration). Le code est déjà pas mal optimisé: pas de boucles inutiles (je crois qu’il ya une seule boucle de taille 2 dans tout l’algo de merging/splitting), il fait une recursivité au niveau des splittings de triangles (qui souvent ne dépasse pas 10, moyenne de 3 environ).

Dois je passer par du code unsafe? On y gagne vraiment beaucoup? (j’hésite vraiment, si je codes en C# c’est justement pour éviter de m’emmerder avec ces conneries de pointeurs).

[quote]Ces fonctions sont appellés souvent, et usent et abusent des références entre objets (par exemple un triangle possède une référence sur chaque voisin, le quad dont il fait partie, le quad parent, et j’en passe et des meilleures). J’ai aussi beaucoup de getter et setters (pure) pour mes objets triangles.

Ma question est: comment optimiser cela pour que cela aille plus vite?[/quote]

Virer les references entre les objets et diminuer severement ton object count. Virer les virtuals, virer les getter et les setter, virer la recursivité. C’est autant valable en C++ qu’en C# ou tout autre langage objet. Pour apprendre c’est bien, mais en vrai, un mesh c’est un tableau de float de taille fixe et un/des tableau(x) d’index. Pas des objets triangles avec des objets quads et tout le toutim :P.

Et surtout, surtout, lancer un profiler et regarder la ou tu perds du temps. L’optimisation ca s’invente ou ca s’intuite pas (enfin la a ce niveau, c’est assez evident pourquoi ca rame). Mais en regle general, on trouve ou ca rame avec des preuves :stuck_out_tongue:

En dernier recours, quand t’auras epuisé toutes les possibilites d’amelioration dans ton algo et ta structure de donnees tu pourras voir du cote du unsafe, mais t’as encore plein de marge la.

Edit: precisions

Oui nan, l’objet triangle n’est pas un truc constitué de vertexes mêmes (ca je m’en doutes bien que c’est pas efficace). En fait, Triangle et Diamond sont deux classes qui sont des containeurs a variables (index du vertex, positions, etc…). Pour l’affichage j’utilise un vertexbuffer + indexbuffer; tu verras avec les résultats du profiler que le problème ne vient pas du rendering mais bien des split et merge.

Voici les infos retourné par le profiler:
Update.Noise average execution time: 3,07714903499825E-05, called 4502 times. Total time: 0,138533249555621
Update.Split average execution time: 0,00985244757169758, called 934 times. Total time: 9,20218603196554
Update.Split.One average execution time: 1,00290221016207E-05, called 2106 times. Total time: 0,0211211205460131
Update.Split.Two average execution time: 0,000754144801088261, called 2106 times. Total time: 1,58822895109188
Update.Split.Three average execution time: 0,00109452492804171, called 2106 times. Total time: 2,30506949845585
Update.Split.Four average execution time: 0,00102781994095665, called 2106 times. Total time: 2,16458879565471
Update.Merge average execution time: 0,00546861818517465, called 934 times. Total time: 5,10768938495312
Render average execution time: 0,000557831714680863, called 934 times. Total time: 0,521014821511926

Quelques explications s’imposent:
Update.noise est la fonction fractale qui indique la hauteur de la montagne (j’utilise un perlin modifié pour mes tests). 0.1 secondes pour 5000 appels, ca va.
Update.split est la fonction qui coupe un triangle en 2, régénère les voisins, etc… 10 secondes pour 1000 appels, c’est la que ca fait mal. Très mal même. J’ai découpé la fonction en plusieurs sous profils, visiblement ca merde à partir du profil 2 à 4. Voir plus bas le code.
Update.merge est aussi une fonction bouffe temps, mais moins que celle de split.

Comme tu peux le voir, le rendering (régénération du VB/IB en fonction des triangles) est très rapide, le problème ne vient pas d’une mauvaise utilisation du GPU.

Le bout de code incriminé:

[code]Profiler.StartProfiling(“Update.Split.Two”);

OppositeVertex = TriangleVertex;

// Now create two new triangles for the split
Triangle[] NewTriangle = new Triangle[2];
NewTriangle[0] = new Triangle();
NewTriangle[1] = new Triangle();

// Split the first one from Triangle
Triangle.SetPriorityWait(0);
NewTriangle[0].Flags = Triangle.Flags;

Triangles.AddAfter(Triangles.Find(Triangle), NewTriangle[0]);

NewTriangle[0].SetVertices(Triangle.VertexIndex[2], nTriangleVertex, Triangle.VertexIndex[1]);
Triangle.SetVertices(Triangle.VertexIndex[1], nTriangleVertex, Triangle.VertexIndex[0]);
NewTriangle[0].SetNeighbors(Opposite, Triangle, Triangle.Neighbors[1]);
Triangle.Neighbors[1].ReplaceNeighbor(Triangle, NewTriangle[0]);
Triangle.SetNeighbors(NewTriangle[0], NewTriangle[1], Triangle.Neighbors[0]);

if(NewTriangle[0].Neighbors[2].Neighbors[2] == NewTriangle[0])
NewTriangle[0].CopyPriorityInfo(NewTriangle[0].Neighbors[2]);
else
UpdateTriangleOffset(NewTriangle[0]);

if(Triangle.Neighbors[2].Neighbors[2] == Triangle)
Triangle.CopyPriorityInfo(Triangle.Neighbors[2]);
else
UpdateTriangleOffset(Triangle);

Profiler.EndProfiling(“Update.Split.Two”);
Profiler.StartProfiling(“Update.Split.Three”);

// Split the second one from Opposite
Opposite.SetPriorityWait(0);
NewTriangle[1].Flags = Opposite.Flags;

Triangles.AddAfter(Triangles.Find(Opposite), NewTriangle[1]);

NewTriangle[1].SetVertices(Opposite.VertexIndex[2], nOppositeVertex, Opposite.VertexIndex[1]);
Opposite.SetVertices(Opposite.VertexIndex[1], nOppositeVertex, Opposite.VertexIndex[0]);
NewTriangle[1].SetNeighbors(Triangle, Opposite, Opposite.Neighbors[1]);
Opposite.Neighbors[1].ReplaceNeighbor(Opposite, NewTriangle[1]);
Opposite.SetNeighbors(NewTriangle[1], NewTriangle[0], Opposite.Neighbors[0]);

if(NewTriangle[1].Neighbors[2].Neighbors[2] == NewTriangle[1])
NewTriangle[1].CopyPriorityInfo(NewTriangle[1].Neighbors[2]);
else
UpdateTriangleOffset(NewTriangle[1]);

if(Opposite.Neighbors[2].Neighbors[2] == Opposite)
Opposite.CopyPriorityInfo(Opposite.Neighbors[2]);
else
UpdateTriangleOffset(Opposite);

Profiler.EndProfiling(“Update.Split.Three”);
Profiler.StartProfiling(“Update.Split.Four”);

// Set up parent pointers
NewTriangle[0].Parent = Triangle;
NewTriangle[1].Parent = Opposite;

// If either of the original triangles was part of a diamond, it has been destroyed.
if(Triangle.Diamond != null)
{
Diamonds.Remove(Triangle.Diamond);
Triangle.Diamond = null;
}

if(Opposite.Diamond != null)
{
Diamonds.Remove(Opposite.Diamond);
Opposite.Diamond = null;
}

Diamonds.AddLast(new Diamond(Vertexes, Triangle, Opposite, NewTriangle[0], NewTriangle[1]));

LevelOfDetailsHasChanged = true;

Profiler.EndProfiling(“Update.Split.Four”);[/code]

De prime abord, je vois le new qui pourrait causer le problème dans la partie Update.Split.Two? Tout le reste n’est qu’accès et assignation de variables.

Je suis en train de me demander si mon problème ne viendrait pas du déréférencement des Diamond (objet composé de 4 triangles) à la fin de la partie 4?

J’avais oublié de préciser: j’avais testé sans pure getter/setter, aucune augmentation de performance à signaler… la récursivité, je suis obligé pour le moment, je crois que ca me ferait trop de modifications au code pour le faire fonctionner sans (sans compter le fait que ca risque de ne pas marcher :P). Je n’ai pas de fonctions virtuelles (abstraction ou autres conneries non plus) dans ce bout de code là.

Ce que je vérais en truc qui prennent du temps :

  • tes 3 new au début
  • Les différent Find (que j’imagine être une recherche dans un stock de triangle), qui doivent être nécessaires j’imagine :confused:
  • le Remove qui doit également faire une recherche (supputation totale)
  • le new à la fin de la part 4

Je me demande, plutôt que passer par des Remove/Add, tu peux pas tapper dans le tableau à grand coup d’indices, ça pourrait ptet economiser du temps? Et par extension si tu allous un nombre suffisant de triangles, t’aurais pas à faire de new non plus (je suppose ici que Triangles & Diamonds sont des tableaux)

Triangles est une LinkedList (double linked list) et Diamonds est une List (list simple). Je soupconne aussi fortement les new (un autre gars m’a dit ca sur les forums de gamedev) et dans le code source originel, toutes les variables du split étaient en static (et je ne comprenais pas pourquoi, maintenant, je commence à comprendre…)

L’auteur originel utilisait une double linked list, mais j’ai jamais vraiment compris pourquoi… une histoire d’ordonnancement certainement, mais ca doit être modifiable au niveau du code vu que chaque triangle dispose d’un pointeur vers son parent, et je pourrais (peut être) utiliser un tableau fixe avec des indices. Toutefois, ce n’est pas l’optimisation la plus simple à faire (j’avais profilé justement les accès à la liste, c’était vraiment pas énorme…

Je vais déjà essayer de supprimer les new, on verra ce que ca donne.

edit: grammaire

Bon heu… je repete alors: pour moi tu ne dois PAS avoir une classe triangle. C’est une heresie pour un truc qui doit aller vite (surtout une classe, une struct a l’extreme limite (et meme…)). Et je vois pas ou j’ai dit que c’etait une mauvaise utilisation du GPU ou un probleme de rendering. Je me doute bien que c’est pas le probleme, vu que tu en parles absolument pas dans ton premier message. Rien que dans ton bout de code si on comprend bien ce qu’il fait, t’as whatmille allocations/deallocations, tu touches des zones massivements differentes de memoire (donc t’as aucune coherence du cache), t’as des niveaux d’indirections de partout. C’est plus lisible et tout, mais faut pas esperer en tirer qqch de rapide. Au dela des optimisations les plus flagrantes, tu auras rien de dramatique niveau perfs sans changer severement ta structure de donnees et ton algo. Si tu changes ca, ca me surprendrait pas plus que ca de voir un x100…

Donc, bis:

Whatmille? Je crée 2 triangles et détruit parfois 2 diamonds (le reste n’est que copie de références)? C’est si lourd l’alloc/déalloc en .Net? Ou alors la copie de référence ne fonctionne pas comme je pense et c’est carrément tout l’objet qui est copié (ca serait pour le moins curieux)?

[quote]Cil’ date=’ 23 May 2006, 08:52’ post=‹ 473005 ›]
Whatmille? Je crée 2 triangles et détruit parfois 2 diamonds (le reste n’est que copie de références)? C’est si lourd l’alloc/déalloc en .Net? Ou alors la copie de référence ne fonctionne pas comme je pense et c’est carrément tout l’objet qui est copié (ca serait pour le moins curieux)?[/quote]
Une allocation/désallocation, c’est toujours coûteux, que ce soit en natif ou en managed (les coûts sont pas les mêmes, mais c’est coûteux de toutes façons, en tous cas dans le contexte d’une appli temps réel gourmande). Le fait d’avoir un type d’objet « triangle », même si ça ne possède que 3 floats et 2 integers, ça fait quand même une structure à allouer/désallouer, multiplié par whatmille (5000 dans ton cas, mais tu veux évidemment aller bien au delà de ce chiffre). Surtout que le GC va vouloir défragmenter ta mémoire lors des collections, et donc bouger tes objets triangles, et donc remettre à jour toutes tes liaisons sur les whatmille triangles restants. Je sais pas si ça participe en vrai à la perte de perfo (je suis assez newbie en .NET encore), mais c’est un des trucs qui me viennent en tête à chaud, là.
La méthode dont parle GloP, je pense, c’est d’avoir une seule classe « ROAMMesh » qui possède un gros tableau de whatmille floats, et des gros tableaux d’indirection avec whatmille integers. Dans le cas le plus simple, tu fixes une limite à ces tableaux, et tu bourrines les floats et les integers. Quand t’as des triangles qui disparaissent, c’est pas grave, t’as des tableaux avec des « trous », mais comme tu accèdes par indirection d’indices, tu ne tombes pas dedans. Au mieux, tu fais des gros tableaux bourrins de structures (vertex, face, etc.) qui wrappent ces integers/floats, histoire de pas t’emmerder avec des accès par indices chiants du genre « tab[3*i+j] », mais je sais pas trop si ça sera okay au niveau perfs (en C++ ça change pas grand chose, mais en C# je sais pas).

Et sinon, rassure toi, une référence, c’est bien juste une référence… seuls les value types sont copiés.

C’est bon, j’ai pas dit de conneries, Gloppy? :stuck_out_tongue:

Non t’as dis exactement ce que je voulais dire mais en mieux :stuck_out_tongue:

Juste une remarque au passage: VTK gère le LOD, et son code C++ est librement consultable. Alors, c’est vrai, VTK n’a pas nécessairement été conçu dans une optique d’optimisation forcenée, mais s’il s’avèrait qu’il échappait au travers constaté, cela pourrait valoir le coup de regarder comment il fait.

Comme le suggère GloP, il semble que tu sois presque arrivé au bout des optimisations possibles pour l’implémentation choisie…

[quote=“vectra, post:10, topic: 29082”]Juste une remarque au passage: VTK gère le LOD, et son code C++ est librement consultable. Alors, c’est vrai, VTK n’a pas nécessairement été conçu dans une optique d’optimisation forcenée, mais s’il s’avèrait qu’il échappait au travers constaté, cela pourrait valoir le coup de regarder comment il fait.

Comme le suggère GloP, il semble que tu sois presque arrivé au bout des optimisations possibles pour l’implémentation choisie…[/quote]
C’est quoi VTK ?

C’est là:
http://www.vtk.org/

Une gigantesque bibliothèque collectant essentiellement du code académique pour tous les problèmes relatifs à la visualisation de maillages polygonaux ou de volumes de données en général. En plus de la visualisation, un gros paquet de méthodes pour passer d’un mode à l’autre (volumique-surfacique) ou pour faciliter la visualisation dans un mode donné (réduction statique de triangles). Il y a un gros paquets de filtres d’import-export, et quand on regarde bien, il y a énormément d’algos intéressants pour traiter des problèmes connexes (analyse de forme, etc).

Ca se présente sous forme d’une hierarchie de classes C++: donc, en fait, tu dois faire un programme les intégrant si tu veux avoir quelque chose qui tourne (mais c’est quand meme 'achement plus simple que de faire la même chose en OpenGL). Pour les allergiques au C++, il y a des ‹ traductions › java, tcl et python (le code est transformé en C++ à la « compil »).

Le problème, c’est que si c’est gratuit et utilisable même pour des softs commerciaux (GLoP appréciera :stuck_out_tongue: ), la boite qui fait/supervise ca gagne notamment sa vie en vendant la doc. Donc, la doc en ligne est à chier un peu limitée, bien que très structurée et rigoureuse. Ceci dit, quand on cherche bien et qu’on connait/apprend, on est sidéré de voir que, très souvent, le programme bien compliqué de visualisation de données qu’on avait en tête se fait par quelques instanciations de classes et les bons paramétrages…

Le cousin de VTK, c’est ITK, qui s’occupe plutot des méthodes d’extraction de données utiles (la surface d’un organe donné)à partir de données de type image/volume, notamment dans le domaine médical.

Ca poutre, c’est énorme, c’est un outil qui prend vraiment de l’ampleur ces dernières années, même si le décollage a été lent. Ca brasse très très large, et ca va vraiment en profondeur.
Bref, j’aime assez.

Woooot, VTK ça a l’air très intéressant dis moi… Il me semble en avoir entendu parler, mais j’avais jamais été voir en tous cas… T’as d’autres secrets de l’univers à nous dévoiler dans le même genre? :stuck_out_tongue:

Obenoui.
Elvis est vivant et Ségolène n’est pas une vraie femme.

Si tu en demandes… :stuck_out_tongue:

Bon, ok, je…

Un dernier detail sur les timings : un truc qu’on oublie souvent quant on commence a faire du profiling, c’est de presente tes donnes de facon pertinente :

  • choisit un frame rate cible, disont 30fps, ca fait grosso modo 33 millisecond la frame, et exprime tes timing divers et varies en pourcentage de la frame.
  • Soit sur que tu time ce qui t’interesse, et pas autre choses.
  • Utilises des bars ou des graphes pour rendre tes donnes plus parlantes.
    C’est con, mais ca aide a vraiment voir ou est le probleme et a aller au plus efficace…
    Jusqu’au jour ou tu es devant un graph plat a 20FPS et que tu es bien embete…

voila, sinon Glop et Lordabdul on couvert le plus gros, et merci a Vectra pour VTK !

Sur ce, je retourne dans ma grotte !

60! Pas 30 rhooo.

En fait… je suis débile. Mon code est parfaitement fluide (j’aurais du me fier aux infos comme quoi le CPU était pas bouffé à 100%). A l’affichage, je constatais quelques “accrochages” (mon objet tourne sur lui même, et de temps en temps, celui ci se bloque, mais ca reste très très léger).

Le problème vient du fait que j’utilisais, pour tester, Environnement.TickCount pour récupérer l’angle de rotation de l’objet courant. Or cette variable n’est que précise à la milliseconde. Que se passe t’il donc lorsque dans la même milliseconde, j’effectue de nouveau un rendu?

Simple: l’objet va être positionné au même endroit. Ce qui fait que je vais avoir cette impression d’accrochage. Je me suis rendu compte de ce problème en travaillant sur un autre algo de geomipmapping qui produisait ce même accrochage alors que j’affichais peu de triangles (et que j’avais pas toutes ces conneries de Split/Merge activées).