Dans ma carriere de programmeur de jeux videos, je suis tres reconnaissant d’avoir aussi eu a travailler aussi sur console (autre que la XBox) car cela m’a aide a prendre conscience d’une chose tres importante: la memoire.
Marc, un excelent ami travaillant a EA et pour qui j’ai enormement de respect avait coutume de dire que les programmeurs PC etaient des feignasses. Globallement, son argument etait que la plupart du temps, les reponses des programmeurs PC a des problemes techniques etaient d’utiliser plus de puissance, sous entendu que le joueur devra acheter un PC plus puissant, alors que sur console on a pas tout simplement pas ce luxe.
D’ailleurs, si on y pense bien, cette idee n’est pas nouvelle. Deja a l’epoque, Joystick avait un sticker sur certains jeux testes du style “halte au surarmement” pour ceux qui se souviennent
Bref, je ne vais pas epiloguer sur le bien fonde ou pas de cette remarque, mais y’a une chose sur lequel j’etais bien d’accord, venant du monde PC moi-meme, etait comment on considere l’utilisation de la memoire en tant que resource abondante sur PC.
Sur les PCs avec Windows on peut utiliser la memoire virtuelle, globallement cela veut dire que si le PC manque de memoire il va en chopper quelque part, quitte a faire ramer le tout en la prendre sur le disque dur (vous savez, le fameux fichier systeme: pagefile.sys).
Sur console, c’est pas possible, le budget est limite (parfois meme partage avec la carte graphique), du coup, on y prete bien plus attention, que ce soit en terme d’utilisation ou de fragmentation.
Par exemple, sur console, un des tests pour certification est de faire tourner le jeu 24h d’affile sans avoir a redemarrer la console. Aucune certification n’est requise sur PC et il est fort probable que plus d’un jeu a l’heure actuelle rateraient ce simple teste (certaines mauvaises langues diront a cause de Windows, mais je n’en fais pas partie! :))
Pourtant, je dois bien avouer que les contraintes en programmation m’ennuies pour rester poli et se soucier de la memoire disponible en est une de plus (je prefere generallement explorer et repousser les limites plutot que de jouer a l’interieur de celles-ci). En revanche, je dois aussi avouer que j’ai rapidement pris conscience que controler l’utilisation de la memoire est un atout formidable sur PC (oblige sur console).
Cela peut permettre notamment de:
- Gagner en rapidite d’execution
- De debugger plus facilement (memory leak)
- De faire des statistiques sur l’utilisation de la memoire vive et savoir combien on consomme au total
- De budgéter certaines parties du jeu (son/image/gameplay…)
- D’avoir une chance d’adresser la fragmentation de la memoire
- Permettre de migrer sur console plus facilement
…
Le controle de la memoire se faisant relativement facilement, j’ai donc decide de tester ca moi meme avec mon Game Incubator en y inserant un objet responsable de la gestion de memoire (on appelle ca un objet allocateur, ou allocateur) pour allouer et desallouer la memoire.
Utiliser un allocateur change votre facon d’allouez et de desallouez la memoire. Vous n’appeler plus ‘new’ et ‘delete’ directement, mais ‘placement new’, mais je parlerai de comment on s’y prend un peu plus bas.
Question rapidite donc ca donne quoi?
J’ai fait plusieurs tests avec malloc/free d’un cote et mon allocateur de l’autre.
La premiere chose que l’on remarque lorsqu’on profile son allocateur est que ca depend des choix d’implmentation que l’on a fait, aussi ce que je marque ici est a prendre avec des grosses pincettes. La chose importante a retenir est qu’un allocateur peut facilement etre plus rapide que l’allocation de memoire par le system.
1. Meilleurs conditions pour mon allocateur:
Allocations et desallocations lineaires (sans fragmentation) de taille variable (utilisation de rand()) entre 1 et 100 bytes.
100.000 allocations:
- Malloc/Free: 69 millisecondes
- Mon objet allocateur: 28 millisecondes
200.000 allocations:
- Malloc/Free: 139 millisecondes
- Mon objet allocateur: 54 millisecondes
3. Plus mauvaises conditions pour mon allocateur:
Allocations et desallocations avec random access (fragmentation de la memoire) de taille variable (utilisation de rand()) entre 1 et 100 bytes:
50.000 allocations:
- Malloc/Free: 10 millisecondes
- Objet Allocateur: 281 millisecondes
100.000 allocations:
- Malloc/Free: 116 millisecondes
- Objet Allocateur: 21488 millisecondes
Comme on peut le voir la situation varie enormement en fonction des choix d’implementation avec une degradation des performances enormes dans des circonstances qui sont peu favorables pour mon allocateur, a savoir, memoire fragmentee (pleins de petit bouts de memoire libres mais pas assez gros pour les allocations demandees).
En revanche, dans le meilleur des cas, l’allocation par le systeme est inferieur en terme de performance, il est difficule de couvrir tous les cas mais on peut facilement avoir un boost en performance pure si on y tient avec un allocateur.
Le system fait pleins de tests avant de retourner la memoire.
Donc comme presque toujours le resultat depend aussi en partie de comment vous utiliser vos outils, je sais qu’en ce qui me concerne je suis souvent dans le meilleur cas avec mon allocateur. La desallocation (qui est plus couteuse avec moi, se fait generallement a la fin d’une partie ou c’est moins important).
En tant que programmeur PC, j’etais habitue a allouer et desallouer de la memoire quand bon me semblait, et clairement allouer/desallouer de la memoire prends du temps en plus des resources.
D’ailleurs, la politique de Criterion en ce qui concerne leurs jeux (BrunOut et Black) est tres simple et pourtant geniale:
Pas d’allocation ou desallocation pendant les sequences de gameplay et d’autres part, des donnees dans le format memoire de la platforme. A savoir, on charge un bloque de data directement dans la memoire sans s’embeter a remplir la memoire en fonction des donnees qu’on lit (ce qui donne un gain en temps de chargement en plus). Apres on fix-up/fix-down pour les pointeurs mais c’est un autre sujet.
(Ils ont aussi la politique de pas utiliser de constructeur et de destructeur mais ca c’est plus par stupidite et ignorance vis a vis du C++, j’appelle ca le syndrome du programmeur C qui passe au C++ sans reellement y passer ;))
Le resultat est un jeu tres rapide et donc plus de puissances pour d’autres choses, ce qui se ressent par rapport a la concurrence.
Si implementer ce systeme vous interesse un allocateur peut etre votre ami, il est alors tres simple d’ajoutter un message qui dit que vous etes en train d’allouez ou de desallouez de la memoire pendant la phase de gameplay, chose impossible sans maitrise de la memoire.
Mais ce que j’aime le plus avec un allocateur, c’est de pouvoir a tout moment afficher le contenu contextuel de la memoire (combien et d’ou vient l’allocation de memoire).
Pour illustrer ce que je dis, voici une capture de la memoire (dans le format de mon choix) de mon jeu en cours de developement pris aujourd’hui:
| Contexte | Taille en octet |
—————————————————–
| FRAMEWORK | 412 |
| ALTITUDEMAP | 412 |
| OOSLOG | 416 |
| USERINTERFACE | 416 |
| GI::DISPLAY/TEXTUREMANAGER | 420 |
| INVENTORY | 420 |
| NAVIGATIONMAP | 424 |
| FLYINGNUMBERSYSTEM | 424 |
| ENERGY | 424 |
| INVENTORYINFO | 424 |
| DOWNLOADMANAGER | 428 |
| TCPSESSION | 428 |
| SKILLSINFO | 428 |
| SKILLCOMPONENT | 432 |
| GLOBALPROCESS | 436 |
| GI::ICOMMUNICATION | 436 |
| SKILLHYPNOSIS | 436 |
| SKILLINVISIBLE | 436 |
| SKILLPSIONICWAVE | 436 |
| SKILLPSIONICSTORM | 436 |
| MINIMPAP | 444 |
| GI::SCRIPT | 448 |
| CHARACTERINFO | 448 |
| PLAYERINFO | 480 |
| HEROCOMPONENT | 488 |
| HEROCOLLECTIONSYSTEM | 492 |
| CONTROL | 496 |
| WORLD | 524 |
| GameProcess | 532 |
| GI::DISPLAY/SCENE/OCTREE | 536 |
| GI::APPLICATION | 540 |
| LOCALINFOSYSTEM | 636 |
| GI::DISPLAY/CAMERA | 724 |
| SESSION | 748 |
| REGEN | 856 |
| GI::SOUND | 924 |
| MASTERCOMMUNICATION | 936 |
| GI::KEYBOARD | 1,044 |
| GI::DISPLAY | 1,904 |
| GI::DISPLAY/SHADER | 2,416 |
| HEROINFO | 2,480 |
| PHYSIQUE | 3,024 |
| CHATSYSTEM | 3,064 |
| GIProfiler | 3,232 |
| GI::DEBUGMENU | 3,640 |
| BITMAPFONT | 4,512 |
| PROXYXML | 4,560 |
| GI::DISPLAY/DISPLAYMODE | 5,088 |
| XPLEVEL | 10,272 |
| ATTACK | 10,400 |
| PROXYSPRITE | 10,580 |
| SMARTGO | 11,536 |
| ABILITIES | 11,600 |
| TEAM | 11,760 |
| LIFE | 11,872 |
| GAMEOBJECT | 12,208 |
| AI | 12,400 |
| RENDER | 13,216 |
| PROXYSCENE | 13,216 |
| POSITION | 13,328 |
| GI::DISPLAY/SCENE | 15,120 |
| GI::DISPLAY/FONT | 15,180 |
| GI::DISPLAY/SPRITE | 16,640 |
| GI::DISPLAY/SCENE/OBJECT3D | 19,240 |
| GI::DISPLAY/SCENE/MESH | 19,388 |
| GI::DISPLAY/SCENE/LINE | 19,488 |
| GI::DISPLAY/TEXTURE/TEXTUREREF | 20,984 |
| GI::DISPLAY/TEXTURE | 27,648 |
| GI::DISPLAY/SCENE/SUBOBJECT3D | 30,096 |
| GI::DISPLAY/SCENE/SUBMESH | 40,392 |
| GI::XML | 97,216 |
| NAVNODE | 174,924 |
| COLLISIONMAP | 208,512 |
| GI::TLIST | 688,004 |
| GI::INETWORK | 789,300 |
| GI::DISPLAY/OCTREE | 5,911,872 |
| GI::DISPLAY/SUBMESH | 8,994,280 |
Comme on peut le voir, la memoire est contextuelle (je sais qui a alloue quoi) et je peux donc faire des statistiques la dessus.
On peut aussi voir que la gestion des listes doublement chainees prend beaucoup de place en memoire (GI::TLIST) ou bien encore que mon jeu ne prend reellement que 17 meg de donnees pure (sans compter la memoire prise sur la carte graphique).
Preuve de cette flemme des programmeurs PC a dompter la memoire, DirectX ne permet meme pas de fournir un objet allocateur, c’est plutot decevant.
C’est aussi utile pour voir ce qu’on doit optimiser, il se peut qu’un objet soit plus gros qu’on ne le pense et alloue pleins de fois sans qu’on ne l’ait jamais suspecte ou plus simplement pour verifier que le jeu ne grossit pas a chaque nouvelle partie et tiens dans une limite de taille.
Puisque la memoire est desormais contextuelle et controllee, faire la chasse aux memory leak (memoire qu’on a oublie de desallouer) devient un jeu d’enfant.
L’objet allocateur peut vous dire s’il lui reste des trucs ou pas lorsque vous detruisez l’objet et qui a alloue ce qui lui reste. Vous pouvez egalement faire un dump entre chaque nouvelle partie pour voir ce qui n’a pas ete libere. Si votre format est en texte, un simple diff sur fichier suffit pour voir tout d’un coup. Simple et efficace.
Certains me diront comme a la BNP Parisbas qu’ils preferent acheter des utilistaires qui coutent les yeux de la tete pour tracer les memory leaks, que personne n’utilise en continue car ca prend plus de temps a l’execution et qu’on peut pas se permettre de livrer une version compilee avec ces utilitaires (en plus du probleme de nombre de licence). Et la du coup (encore) j’ai un peu pitier pour eux lorsqu’on sait a quel point c’est simple d’utiliser un objet allocateur.
Vous pouvez egalement logger toutes les allocations ou desallocations qui se passent pendant la phase de gameplay pour faire des reports ou bien dumper la memoire pour analyser/comparer entre deux pc puisque desormais vous savez la memoire utilisee.
Je parlais aussi de l’avantage de budgeter. Lorsque vous utilisez un allocateur, car vous n’etes pas limite a un seul objet. Ainsi vous pouvez en avoir un pour les graphisme, un pour le son, un autre pour le gameplay, un pour le systeme… et attribuer un budget pour chacun d’en eux.
Si vous avez peur de la taille maximum que vous pouvez allouer, puisqu’un allocateur prealloue une certaine taille de memoire avant de l’utiliser, sachez que ca depend de l’implementation de celui-ci.
Par exemple, mon allocateur permet de s’etendre si la memoire de base ne suffit pas (il rajoutte alors une autre page de x megs, ce qui marcherait donc pas sur console ou vous prenez tout des le debut et c’est tout ce que vous avez).
Ok, convaincu ou curieux de savoir comment ca marche? Vous allez voir, le principe est tres simple et facilement applicable (avec un bemol pour STL).
Imaginons que j’ai une class BLAH:
class BLAH
{
… constructeur/destructeur/fonctions/variables…
};
Dans votre programme, vous etes habitue a faire:
BLAH *blah = new BLAH(param1, param2);
Avec un allocateur la logique est la suivante:
1. allouer de la memoire:
void *memoire = Allocateur->Alloue(sizeof(BLAH));
2. appeler le constructeur de BLAH dans cet espace memoire en utilisant ‘placement new’:
BLAH *blah = new (memoire) BLAH (param1, param2);
Comme vous voyez c’est pas tellement plus complique, ce n’est pas new qu’on appelle, mais placement new qui demande la memoire a utiliser. Votre objet allocateur peut egalement demande un contexte pour l’allocation et logger le fichier et la ligne egalement.
Du coup, cela devient:
void *memoire = Allocateur->Alloue(sizeof(BLAH), “Allocation de BLAH “, __FILE__, __LINE__);
Comme ca vous tracez d’ou vient l’allocation sans faire appel a la callstack (qui ne serait pas accessible en release avec toutes les optimisations de toute facon).
Ok alors la tout de suite on me dit que c’est un peu lourd a ecrire et je suis d’accord. C’est pour ca que j’utilise une macro qui fait tout ca pour moi:
BLAH *blah = GIMEMALLOC(BLAH, “Allocation de Blah”)(param1, param2);
La ca devient juste un changement de notation et demande un peu de rigueur pour entrer un contexte, meme si comme on a vue avec mon dump, le contexte a pas besoin d’etre super explicite non plus.
Pour desallouer il faut faire la meme manipulation a l’envers.
La ou vous etiez habitue a faire:
delete blah;
blah = NULL; // ouais meme dans les exemples faut pas oublier
Il vous faut faire a present:
1. Appeler vous meme le destructeur!
blah->~BLAH();
2. Liberer la memoire que vous avez prise
Allocateur->Desalloue(blah);
blah = NULL;
La encore votre pote la macro vous fait le tout en une ligne:
GIMEMUNALLOC(BLAH, blah);
Notez que puisque vous devez appeler le destructeur, le type du pointeur est donc requis (pas de soucis pour l’inheritance du moment que votre class de base a un destructeur virtuel… et elle a interet a en avoir un de toute facon, objet allocateur ou pas).
(Edit: Allez voir les commentaires pour une facon elegante de ne pas avoir besoin de specifier le type de la classe dans la macro)
Bon attention tout de meme aux petits malins pas si malins. De la meme facon qu’on ne melange pas des new avec des free et des malloc avec des delete, vous ne pouvez pas mixer les deux methodes, a savoir allouer avec un objet allocateur et utiliser free ou delete pour desallouer (et inversement).
Notez donc que votre objet allocateur a besoin que de deux methodes:
Alloue(taille);
Desalloue(pointeur);
Si vous implementez ces deux fonctions avec malloc et free, vous pourrez tester tres facilement l’utilisation d’un objet allocateur sans avoir a s’embeter avec l’implementation d’un vrai allocateur qui peut venir plus tard.
Si vous etes plus curieux a propos de l’impementation d’un objet allocateur, vous pouvez voir le mien ICI.
Bon tout n’est pas rose comme vous pouvez vous en douter. Si vous utiliser STL, c’est plus problematique car il vous faudra fournir un custom allocateur et la syntaxe devient assez horrible (bien que ca se typedef).
De plus faire un custom allocateur pour STL c’est pas si simple, je vous invite a regarder ce document d’un ami la dessus: Custom STL Allocators
Pour l’instant je m’en occupe par car mon utilisation de STL se limite dans les classes et que j’utilise mon allocateur principallement pour tracer les memory leak (entre partie et a l’exit du programme), ce qui fait que je fais confiance a STL pour se demerder correctement en interne. Ouais c’est pas bien et faudra que je fixe ca un jour pour avoir la vrai taille de mon programme.
Un des soucis majeur reste l’utilisation de librairies qui n’ont pas ete developees en interne et qui ne fournissent pas la possibilite d’utiliser un allocateur.
C’est la chiotte dans ces cas la et il faudra s’en remettre a l’operateur global new et delete (car oui vous pouvez les overloader).
Mais, pourquoi ne pas tout simplement overloader l’operateur global new et delete pour tout me direz vous plutot que de s’embeter avec un objet allocateur?
Tout simplement car c’est loin d’etre une solution ideale a mes yeux et surtout pas sur du long terme. Certes ca peut etre necessaire pour une fin de projet lorsque de toute facon il est trop tard pour tout converir, mais ca ne doit pas etre une approche systematique.
Le premier probleme est que je ne peux pas utiliser plusieurs pool de memoire. Donc oublier la possibilite de budgetter.
Ensuite, y’a pas d’aide contextuelle gratuite, va vous falloir recupeter la callstack pour savoir d’ou vient l’appel et ca sera tellement lent que vous lancerez sans cette aide au debuggage la plupart du temps, rendant cette possibilite bien moins interessante pour le coup (c’est ce que font les programme pour tracer les memory leak et personne les utilisent constamment a cause de ca).
L’autre probleme est qu’il ne peut etre overloade qu’une seul fois par executable, donc en gros vous utilisez directement votre derniere chance de pouvoir capturer des information sur la manipulation de la memoire. Impossible de linker plusieurs librairies qui utilisent cette technique. Bref, c’est tout naze.
Pour moi l’operateur global new et delete doit etre overloade pour une seule chose: afficher une erreur afin d’empecher son utilisation.
En conclusion, ceux qui maitrise la memoire sur PC ont clairement une longueur d’avance niveau optimisation et debuggage vis a vis des “feignasses”
et le tout avec tellement peu d’effort que ca revele presque de l’insolence.
Quel post!
Très intéressant.
Il y a aussi la librairie Boost qui contient des trucs pas mal sur l’allocation/désallocation de mémoires et sur la gestion des pointeurs.
De plus lorsque je regarde plein d’annonces d’emploi de programmeurs dans le jeu vidéo (je cheche en Angleterre en ce moment) il y est spécifié bien souvent : “Good understanding of memory management and processor allocations”.
@Jeremy
“Pas d’allocation ou desallocation pendant les sequences de gameplay et d’autres part, des donnees dans le format memoire de la platforme. (…) Le resultat est un jeu tres rapide et donc plus de puissances pour d’autres choses, ce qui se ressent par rapport a la concurrence.”
Ne me dit pas que tu croies sincèrement que la concurrence fait des allocations dynamiques pendant les séquences de gameplay? Même sur Adibou PS2 il n’y en a pas. Je le sais, j’en ai dégagé quelques unes qui trainaient en fin de projet :=)
Autre chose: On va dire que tu as la page de 4 octets suivante: 0xFF 0×00 0xFF 0×00. Tu as donc 2 octets libres mais non consécutifs, ta mémoire est donc fragmentée. Tu demandes une alloc de 2 octets consécutifs. Tu fais quoi dans ce cas là? Tu augmentes la page de X octets? Sur PC ok, mais sur console? Je demande parce que je vois pas de solution à ce problème, à moins de tout réordonner “intélligemment”. Si c’est le cas, qu’utilises tu comme algo? Tu combles les trous ou bien tu bouges tout “vers la gauche” ou autre?
Perso j’overloade new et delete et j’utilise une macro MY_NEW new(__FILE__,__LINE__) pour allouer et le delete standard pour désallouer.
“Impossible de linker plusieurs librairies qui utilisent cette technique”: je vois pas pourquoi. ODE overloade new et delete et pourtant je linke dessus sans problème.
“Ensuite, y’a pas d’aide contextuelle gratuite”: de quoi s’agit-il? Dans tes macros et ton rapport non plus tu ne parles pas de CallStack?!
En fait à part pour budgetter je vois pas l’interêt. Remarque c’est déjà pas mal et ça me fait presque envie d’en faire un :=)
Ah si, pour la vitesse d’execution aussi. Mais là, tes chiffres dans les mauvaises conditions me font peur. Souvenez vous de la loi de Murphy: si ça peut aller mal, ça va aller mal. Donc ne surtout pas supposer que tout toujours va se passer dans le meilleur des cas.
Je fais partie des fainéants, je fais seuleument des allocateurs pour des cas particuliers (genre des petits objets ayant tous la même taille et ayant un cycle de vie très court) ; pour le reste mon expérience sur PC me dit que c’est pas trop la peine de se prendre la tête, l’allocateur par défaut était pas trop mauvais.
Bon maintenant sur console, c’est une toute autre histoire (oui je fais aussi de la programmation calculette en ce moment).
Très intéressant cet article, ça va bien me servir pour les jeux sur téléphone portable que je vais développer :p
Drajul:
Houla beaucoup de choses a repondre, t’es en forme Drajul
“Ne me dit pas que tu croies sincèrement que la concurrence fait des allocations dynamiques pendant les séquences de gameplay? Même sur Adibou PS2 il n’y en a pas. Je le sais, j’en ai dégagé quelques unes qui trainaient en fin de projet”
Pas un projet sur lequel j’ai bosse avait une politique de 0 allocation/desallocation pendant les phases de gameplay.
Content de voir qu’Adibou etait a la pointe des techniques
Pour ton probleme d’octet je dirais que c’est un fau probleme.
1. Si tu dois allouer t’es pas en phase de gameplay donc tu peux prendre tout ton temps pour resoudre le probleme (peu choisissent de rearranger la memoire a cause des pointeurs, mais certains le font)
2. Tu as pas de memoire fragmentee si tu te demerdes bien
Ta macro prends la ligne et le nom de fichier, ca revient a avoir un allocateur, meme si ton implementation est d’appeler apres le global new ou delete.
Le context ca serait un argument de plus entre par le programmeur pour dire pourquoi il alloue la memoire, pour moi c’est le nom de la classe en generale
Tu ne peux pas avoir plus d’un global new et delete. Le compilateur le cree au link si il existe pas (un peu comme un constructeur dans une classe si on le fournit pas). Donc tu peux pas en avoir deux ou t’as une erreur de linkage. T’es sur que tu confonds pas avec un allocateur?
Je precise qu’il y’a une difference entre overloader new et delete par classe et overloader le global new et delete.
Stephane: ouais les consoles… comme les DS hein
Comment vous faites pour les memory leak? La methode BNP (je serais decu la! :D)
Hey l’mamouth, si apres tout ce que j’ai dit t’es pas convaincu d’utiliser un allocateur je reste sans voix!
Pour les memory leaks : boost::shared_ptr + code review, et parfois un coup de memory validator ( http://www.softwareverify.com/cpp/memory/index.html ) l’outil est plutôt vachement rapide par rapport à la concurrence (d’ailleurs ils s’en gaussent dans la feature list).
Mais bon comme dit pour les trucs vraiment lourd niveau allocation on a un allocateur maison. Et pour la calto de poche au niveau mémoire c’est tellement babylone qu’on va finir par optimiser l’utilisation de la ram à la main (et je déconne même pas).
Bon je vais arreter de te charier un peu, faut faire gaffe je deviens lourd rapidement
Puis faut que je migre mon GI sous SVN a la place de CVS aussi maintenant que Klaim m’a fait remarque que je pouvais.
Tu devrais essayer pour ton prochain projet Stephane. Ca coute rien d’avoir ton allocateur qui implemente malloc et free (au moins vous pourrez logger) et c’est une bonne pratique, ca vous evite de faire la tactique BNP qui consiste a jetter de l’argent et du temps dans des solutions qui n’ont pas lieu d’exister.
Bonne chance avec ta calculette et envoie nous des screenshots!
Salut,
Je uis entirement d’accord avec cet article. etant professionnel dans le jeu video, et developpant mon propre moteur egalement, le besoin de tracker les memory leak c’est vite fait sentir
la kjAPI utilise un systeme similaire, a base de macro pour allouer et desallouer (il est possible pour l’utilisateur de definir ses propres allocateurs).
Par contre juste un “tips” a propos de la mecro de liberation:
En utilisant la puissance des template, il n’est pas necessaire d’avoir a repasser le type pour l’appel du destructeur:
Voici celui de la kj :
#define kjDelete(_ptr) if (_ptr) { kjCallDtor(_ptr); kjFree(_ptr); _ptr=NULL; }
//————————————————-
// generic explicit destructor call
//————————————————-
template inline void kjCallDtor(T* _pPointer)
{
_pPointer->~T();
}
Dans ce cas le compilateur utilisera le type de la variable pour appeler son destructeur
Bon code a tous
Jeckle
Salut Jeckle et bienvenue ici!
Merci pour le tip, c’est tout con mais fallait y penser.
Avant ca me genait pas des masses de marquer le type mais maintenant que j’ai vue une solution elegante ca va me gener beaucoup! Je vais changer ca de ce pas
On y perd un peu si le compilo ne suit pas le conseil d’inline mais rien a faire, on est pas sense s’amuser avec la memoire pendant les phases de gameplay
Merci encore Jeckle, c’est tres appreciable!
Décidément tu y tiens à Drajul/drajul.
C’est d-a-r-j-u-l :=)
“Content de voir qu’Adibou etait a la pointe des techniques :)”
Oui sauf qu’on utilisait RenderWare Studio, RenderWare Graphics, RenderWare Physics et RenderWare IA. On se rattrape comme on peut
“Je precise qu’il y’a une difference entre overloader new et delete par classe et overloader le global new et delete.”
Effectivement après vérif, ODE passe par une structure, donc pas étonnant que j’ai pas eu de problème de link. Faut que je fasse gaffe moi
“Tu as pas de memoire fragmentee si tu te demerdes bien”
Vas y j’t'écoute
“Ta macro prends la ligne et le nom de fichier, ca revient a avoir un allocateur”
Cool j’en utilisais un sans le savoir ! Mais il me manque la feature budget et c’est plus fort que moi, ce dont je me suis passé jusqu’à présent me semble tout à coup indispensable
Hop voila tout est change, merci encore Jeckle. Si t’as d’autres tips sympa comme ca hesite pas
Pour les debutants en prog qui voudraient copier la fonction, oubliez pas le sinon le compilateur va pas comprendre T
template
inline void CallDestructor(T* _pPointer)
{
_pPointer->~T();
}
Hmm la feature budget tu pourrais aussi l’implémenter en faisant que ta macro appelle une fonction templaté avec une implémentation générique, puis une spécialisation par type d’objet qui loggue l’usage mémoire de ces objets, vu qu’en principe les différents sous système alloue des types d’objets différents.
C’est pas une solution parfaite mais elle a l’avantage de pouvoir s’intégrer rapidement dans le code que tu as écris.
darjul petit d, pour eviter de fragmenter te suffit de pas desallouer. C’est plus facile a dire qu’a faire ce qui explique que ceux qui y arrivent se demerdent bien
Y’as plusieurs techniques, t’as certains programmes qui se demerdent pour allouer dans le meme ordre et toujours desallouer dans le sens inverse (je crois que ca s’appelle frame allocation ou du genre).
Sinon tu peux avoir plusieurs allocateur pour plusieurs parties du jeu.
Sur black n white on avait dumpe la stack apres loading. Ensuite une fois une partie finie on remplacait directement la memoire avec le dump sans se faire chier avec ce qui etait encore alloue. huhu. Ca permettait de pas avoir de probleme en multiplayer surtout. Mais bon c’etait un peu extreme comme technique et je recommande pas.
Puis comme t’alloue/desalloue pas pendant une partie y’a pas de soucis normallement.
hello,
darjul : “Ne me dit pas que tu croies sincèrement que la concurrence fait des allocations dynamiques pendant les séquences de gameplay? Même sur 4d1bou PS2 il n’y en a pas. Je le sais, j’en ai dégagé quelques unes qui trainaient en fin de projet :=)”, “Content de voir qu’4d1bou etait a la pointe des techniques ”
). Je me suis même fais taper sur les doigts en voulant développer “sans allocation pendant la phase gameplay”. Mais bon c’est le nombre d’années d’XP (à rien glander) qui compte.
j’ai déjà bossé avec des mecs issus d’4d1bou et je peux te dire qu’ils laissent plein d’allocations dynamiques à tout les niveaux du jeu (même pendant la phase gameplay). La leçon de la PS2 ne leur a pas servi (pour une majorité sauf darjul
darjul :”Effectivement après vérif, 0DE passe par une structure, donc pas étonnant que j’ai pas eu de problème de link. Faut que je fasse gaffe moi”
0DE c’est trop naze. ça fonctionne que sur PC (y a du code assemleur x86 à l’interieur)… c bon j’arrete le troll
ps: désolé du poste
@msx
Les mecs dont tu parles, j’aimerais bien connaitre leurs noms. Ma main à couper que soit:
1) Ils ont pas bossé sur Adibou PS2 mais sur d’autres Adibou.
2) Quelqu’un a fait le ménage pour eux sans même qu’ils le sachent (hautement probable).
Mais je peux t’assurer que dans la gold, y’a pas d’allocs pendant le gameplay.
Je suis d’accord, ODE est trop naze (d’ailleurs dans la 0.5 y’a pas d’allocs pendant l’update alors que la 0.7 y’en a une à chaque update, raison pour laquelle je suis resté en 0.5). Mais c’est le seul que je connaisse qui marchotte ET qui soit fournit avec les sources. Et justement grâce à ça, contrairement à ce que tu dis, ça fonctionne pas que sur PC. Mon projet perso tourne aussi sur Xbox (ouais ok c’est x86 aussi mais bon) et en utilisant Bullet à la place de Ice on doit pouvoir compiler aussi sur Xbox 360 (je pense que c’est des vieux asm {} de Ice dont tu parles pour le code x86 ?). Tokamak/Newton/Ageia: les consoles faut oublier puisque ni les sources ni les libs consoles ne sont fournies. Il y a aussi Bullet et TrueAxis, mais je ne sais pas encore ce qu’ils valent. Donc si tu connais un truc super (un ode en mieux quoi), je suis preneur, parce que ODE c’est le seul choix possible dans mon cas, faute de mieux.
Bonjour
J’ai lu avec attention cette article que je trouve vraiment intéressant. Mais en même temps ça chamboule un peu ma méthode préférée qui est comme suit :
template
class Cachable
{
public :
void * operator new (size_t t) throw (int)
{
//…ici on retourne un objet libre du stack
//…ou on envoie un exception si on n’a pas la place
//…qu’on réoccuper la ou on a appeler new pour faire un jolie
//.. output de débugage
}
void operator delete(void * c)
{
//.. on indique que l’objet utilisé est libre maintenant
}
protected :
static void * stack; // le tas d’objet utilisable
};
template
void * Cachable::stack = malloc(sizeof(T)*STACKS_SIZE); //on alloue la mémoire pour le tas d’objet de taille STACKS_SIZE
Ainsi on a plus qu’a dériver une classe a partir de celle-ci comme suit
class Client : public Cachable
L’appelle devient vite encombrant a cause de try catch (mais c’est seulement dans la version debug donc ça va) . En plus pour déterminer la meilleur taille de cache il faut créer une classe équivalente a Cachable mais qui sert juste à logger l’utilisation de la mémoire en fonction de l’état du jeu.
Comme je ne suis pas le plus grand spécialiste de la gestion de la mémoire je me demande la quelle est la plus utilisé dans l’industrie.
Nickaru
Salut Nickaru,
Alors dans l’ordre
1. Si t’as plus de memoire ca sert a rien de prevenir l’utilisateur.
comme dirait un dev principale sur XBox, interceptez pas les exceptions ca sert a rien.
“Desole vous n’avez plus assez de memoire rebooter votre console”
On utilise pas les exception handling pour les jeux (de maniere generalle), c’est une perte de memoire de l’activer.
Pour les outils en revanche c’est plus qu’important pour pas perdre ses donnees.
2. Tu parles en fait d’overload de new et delete de class. C’est en effect possible et recommander pour les objets de la meme taille que tu vas allouer/desallouer souvent. C’est assez courant sur console mais la generalisation que tu fais me semble etre plus lourde qu’interessante.
Maintenant l’allocateur dont je parle dans l’article devrait etre appele dans ton implementation de new et delete de classe.
Tres interessant article !
je decouvre un peu ce blog au hasard, un petit de plus pour mon agregateur RSS ^_^
je vais un peu plus fouiller, mais ca fait plaisir de trouver un blog de programmeur dans le jeu, bonne continuation !
Salut,
1)Le signalement pour le manque de mémoire n’est activé que pour la version debug et les beta test. Juste pour être certain qu’il y a bien assez de mémoire.
2)La méthode vient de la déclaration de singleton que j’avais trouvé dans un bouquin. Mais c’est vrai que si de nombreuses classes héritent de ce template, j’aurais beaucoup de définition des méthodes.
Je crois que je vais effectivement mixer les méthodes. Utiliser la surcharge pour les objets dont je suis certain à 100% du nombre (ex. je ne veut pas qu’il y ait plus de 1024 Clients en même temps) et ta méthode pour tout le reste.
Et vraiment l’implémentation dans GI est vraiment très propre et agréable à lire (Infiniment plus que certain jeu commerciaux cf quake). GI à l’air assez simple à prendre en main, faut que je regarde l’ensemble de plus prêt.
Nickaru
Coucou Nickaru, decidement tes posts sont choppes par le filtre anti-spam.
Pour le 1) tu alloues normallement toutes la memoire dispo sur console lorsque tu crees ton allocateur.
Apres c’est simple de savoir quand y’a plus de memoire, ton allocateur te le dit et t’as pas eu besoin de t’embeter avec des exceptions.
Ca sert a que dalle les exceptions handling pour des jeux sur console, a part rendre le code plus difficile a debugger et perdre de la place memoire (meme si c’est qu’en debug)
2) Comme disait Mark (ouais le meme pour lequel j’ai enormement de respect) les singletons sont la sources du mal hehe, mais bon je reviendrai une autre fois la dessus.
Bah dis donc, content que tu trouve le GI tres propre. Generallement les gens ont un peu de mal a comprendre comment on utilise des interfaces en C++.
Quake c’etait du C quoi qu’on en dise donc c’est clair que c’est pas vraiment ce qu’il faut prendre comme exemple si on veut progresser en programmation oriente objet
Merci pour les compliments sur le GI, ca fait plaisir. le GIMemory est l’une des dernieres libs que j’ai faite donc les plus vieilles sont bien moins propres malheureusement et pas vraiemnt le temps de tour reconcevoir.
Et comme disait encore Mark lorsqu’on faisait du code review ensemble (decidemment une grande source d’inspiration pour moi en prog ce Mark), la seule chose qui compte c’est les headers file et la defintion des interfaces car si c’est bon le reste (comprendre l’implementation) peut etre changee si besoin est sans impacte.
Marrant, je viens de passer un peu de temps aussi la dessus
En fait je trouve qu’en C++ c’est vraiment pas pratique de faire son gestionnaire memoire. Il y a tout plein de petits details qui font qu’il faut faire vachement gaffe. C’est vraiment pas pense simple.
Par exemple, quand tu fais un operateur placement new, tu as interet a faire le delete “equivalent”, sinon si il y a exception dans le constructeur appele par un placement new, la memoire allouee avant ne sera pas liberee.
Autre chose, j’utilisais aussi l’appel du destructeur explicite “myClass->~MyClass()” mais evidemment un client a voulu pouvoir utiliser le new sans differencier les classes et les types C primitifs. Autant dire que “a->~int()” ca compile pas vraiment ! J’ai donc du changer de systeme et ca s’est avere etre une vraie galere.
Sans compter que si tu veux gerer les tableaux de classe, ca devient vraiment subtil suivant si ta classe a un destructeur ou pas par exemple…
Au final on y arrive, mais quelle prise de tete, surtout quand on travaille avec des environnements “exotiques” genre Symbian qui redefinit deja plein de trucs.
Petite precision: chaque dll peut avoir son new/delete global. Ce n’etait pas tres clair dans les commentaires.
Au final c’est super, mais si on veut faire les choses bien, c’est pas si evident que ca en a l’air a premiere vue.
Bon alors la methode du template, j’y avais pense pour eliminer le besoin du nom de classe dans la macro de delete mais ca ne resolvait pas mon probleme. Mais la c’est trop fort
Je resume…
Mon client ne voulait pas de macros differentes pour delete les types C primitifs (ou les classes sans destructeur mais ca c’est pas conseille) et les classes. Du coup j’avais fait un delete global, mais j’avais un probleme sur Symbian qui avait deja redefini le delete global, qui se trouve dans une lib et non une dll… Juste a cause de Symbian, j’allais etre a deux doigts de faire deriver toutes les classes (les miennes plus celles du client) d’une classe vide qui redefinissait juste l’operateur delete, avec toutes les merdes eventuelles (integrations d’autres libs, creation d’une classe derivant de deux classes differentes…).
Et la je me suis dit, je perds rien a essayer le coup du template… Et BINGO !!! Le compilo, quand il genere le code template, elimine tout seul comme un grand la ligne qui contient _pPointer->~T() si il s’agit d’un type primitif ou d’une classe sans destructeur
Du coup plus besoin de creer de classe de base, ni d’avoir deux macros de delete differentes… Le bonheur… Teste sur compilos Microsoft et gcc…. Trop fort, je pars en week end heureux…
En revanche implementer l’appel des destructeurs dans le delete d’un tableau de classes, ca fait une macro absolument horrible !!!
Voili, voilou
Salut Michael
“Par exemple, quand tu fais un operateur placement new, tu as interet a faire le delete “equivalent””
Oui mais ca c’est normal, c’est ce que je disais dans le post, tu melanges pas new et free ou malloc et delete non plus.
Juste pour info le destructeur est automatically ajoutte lorsque tu as une class (ou meme une struct) et que tu compiles en C++.
Je doute que la ligne de la template d’evanouisse
Mais si ca pose probleme t’es pas oblige d’utiliser la macro non plus! Des fois j’alloue un bloque de memoire comme avec malloc, faut que j’utilise mon allocateur.
Promis, mon prochain topic en prog sera plus simple
Je me suis mal exprime
Oui faut pas melanger new/free et malloc/delete. Ce que je vouais dire c’est ca (exemple concret):
Tu crees operator new(int size, void* mem, int dummy).
Et bien dans ce cas, tu as interet a creer un operator delete(void* ptr, void* mem, int dummy). Pourquoi ? Si jamais une exception est lancee dans le constructeur (donc juste apres TON operator new), si le delete correspondant n’existe pas, la memoire ne sera pas liberee. Si ce delete “associe” existe, la memoire sera correctement liberee. C’est de ca que je voulais parler. Sinon si tout se passe bien, c’est toujours le delete “normal” qui est appele
Par rapport au template, j’ai egalement utilise la macro sur un int
Et ca compile ! Donc si la ligne de template s’evanouit
Essaye tu verras …
Non non tu peux continuer a rester a ce niveau ca ne me derange pas ^^
Etonnant qu’un article sur la memoire (tres bon au prealable) ne parle pas de l’utilisation de mem pools pour les allocations de petite taille
Par contre je ne suis pas sur d’avoir compris l’interet des macros ?
pourquoi ne pas simplement surcharger new et delete, je me vois mal integrer un gestionnaire memoire dans un projet existant et devoir repasser sur tous les news du codebase et les remplacer par le define correspondant !
Salut ox et bienvenue ici.
Merci pour le compliment et ta remarque est tres juste a propos de pool de memoire que l’on utilise souvent lorsqu’on a beaucoup d’objets de meme taille (pas forcement en fonction de leur taille memoire).
En revanche je ne suis pas sur que tu aies lu l’article jusqu’au bout en ce qui concerne la surchage de global new et delete et notamment:
“Mais, pourquoi ne pas tout simplement overloader l’operateur global new et delete pour tout me direz vous”
Les macros permettent d’eviter d’avoir a ecrire deux lignes a chaque fois et de pouvoir changer la syntaxe sans avoir a changer le code si besoin etait.
Bonjour,
En fait je crois qu’on c’est pas compris sur ma méthode.
Pour les singletons je ne comprend pas vraiment leur utilité non plus (plus par manque d’expérience mais rien que le fait qu’on empile le pointer this a chaque appelle de méthode me trou le c** autant utiliser une classe statique si l’objet est unique), mais ça permet de déclarer une instance unique. C’est ainsi que je me suis dit plutôt que de déclarer une seule instance de ma classe autant en déclarer un pool de mémoire.
Le résultat est le template présenté plus haut.
Donc si j’ai une classe A en la déclarant ainsi :
Class A : public Cachable inf A, 1024 sup (inf et sup sont les signes off course)
Et donc A hérite automatiquement des opérateur new et delete du template et il possède son propre pool de mémoire.
Pour les exceptions, j’en génère (en mode debug :D) dans 2 cas quand le pool déclaré n’est pas suffisant (plus de 1024 instances de A pour l’exemple). Et quand la taille demandé n’est pas égale à la taille du type du pool (cf. on a une classe B qui hérite de A sans surcharger son propre Cachable). Et si j’ai un gros pb il me suffit de virer l’héritage du template Cachable et hop c’est les new et delete normaux qui prend en charge la mémoire)
Mais je crois que je vais utiliser ta méthode pour les objets dont il est difficile d’être certain du nombre cf les nœud de A*, comme ça je suis certain que le bloc mémoire qui me reste est efficacement géré).
Pour GI c’est ou qu’on en parle?
nicolas
Coucou Nickaru,
Ne confonds pas design et implementation. Un singleton c’est un objet qui n’a qu’une seule instance. La ce que tu discutes c’est la facon d’implementer ca. Donc tu peux pas dire “les singletons c’est mal car ca appelle …” sans faire cette grosse confusion.
Ensuite pour ta classe template de laquelle on derrive ouais c’est en effet ce qu’on fait, mais rien n’empeche cette classe de reserver cette memoire avec ton allocateur plutot que celle systeme.
Pour le GI j’ai vire le forum, trop de spam et pas le temps de gerer ca, donc envoie un email si t’as un soucis particulier, mais je le dis tout de suite faut pas etre presse pour les reponses
Plus la reponse demande du temps plus je risque de trainer les pieds pour repondre.
Merci aux abrutis de spammer de comprendre qu’on s’en branle de vos links a la con et que tout est modere ici donc y’a aucune chance que j’en laisse passer… Tout de meme 150 qui ont essayes deja… mais bon la je digresse
Je ne suis pas un pro, mais le genialissime valgrind (http://valgrind.org/) traque les leaks, les acces memoire invalides, et tout ce qui pourrit la vie des codeurs. Au passage, il fait aussi du profiling et tout plein d’autres choses…
Tant que j’y suis, je suppose qu’il n’y a pas de portage Linux/BSD de prevu ?
Tiens je rebondis rapidement sur ce post.
Hier j’ai programme une petite application pour le GI que j’inclurai dans mon prochain SDK et qui permet de voir l’etat de la memoire en temps reel. Ca permet notamment de voir si la memoire fragmente ou si y’a un usage intensif de la memoire qu’on ne suspectait pas.
J’ai d’ailleurs fait une petite video de Battle For Independence (mon projet actuel donc) afin de montrer comment ca marche.
http://gi.kamron.net/Videos/GIMemMonitor.zip
L’application qui permet de voir l’etat de la memoire est une application independente (qui marche grave au GIMemory) sur la droite du jeu.
Code de couleur:
Noir, c’est memoire intouchee
Rouge, c’est memoire allouee
Blue, c’est memoire desallouee
Si vous etes attentif (pas facile vue la qualitee de la compression), vous verrez que pendant le jeu y’a encore pas mal d’allocation/desallocation en continue. Va falloir que je repare ca.
PS: non aucun portage prevu