Fermer2
BrunniLe 20/05/2013 à 17:58
Puisque j'ai pris du temps pour en parler dans le topic du presse papier je vais mettre ça à un endroit plus adapté smile
Donc le problème bien connu quand on fait du C++ c'est la gestion de mémoire et en particulier le fait que chacun fait un peu à sa manière. Un truc que j'aime bien, pour ceux qui connaissent pas, c'est la gestion de mémoire d'objective-C et je voulais essayer de reproduire ça pour un projet C++, histoire de voir.
Donc le principe est simple, chaque objet a un compteur de références, quand vous voulez la "propriété" de l'objet, vous l'incrémentez (retain), sinon vous le décrémentez (release). Par défaut tout objet que vous recevez (valeur de retour ou paramètre) n'est pas "à vous", ça signifie que vous pouvez l'utiliser le temps de votre fonction, mais vous ne pouvez pas vous attendre à ce qu'il continue d'exister car quelqu'un d'autre en est propriétaire. Si vous voulez le garder pour plus tard, il faut le "retenir".
class StringPrinter {
    char *str;
public:
    C(char *param) {
        str = retain(param);
    }
    ~C() {
        release(str);
    }
    void print() {
        printf("%s\n", str);
    }
};

Alors que pour les cas de base :
void function(char *param) {
    /* rien à faire ici, la string n'est pas à nous mais on peut s'en servir */
    printf("%s", param);
}

Bien sûr, il faut éventuellement faire gaffe à libérer ce qu'il y avait avant.
static char *keptStr;
void setStringToPrint(char *param) {
    release(keptStr);         /* release previous */
    keptStr = retain(param);  /* keep this one current */
}
void print() {
    printf("%s", keptStr);
}

Tout ça marche merveilleusement bien pour les paramètres, mais pour les valeurs de retour c'est plus compliqué : si une fonction crée un objet juste pour la retourner elle en est la seule propriétaire. Lorsqu'elle se termine, elle doit donc logiquement le libérer. Problème alors, il n'appartient plus à personne et va être détruit. Pour pallier à ça on l'inscrit dans un pool d'autorelease, qui garde une liste d'objets qui n'appartiennent plus à personne mais laisse une chance avant de les détruire. Il est donc possible pour les appelants d'utiliser l'objet jusqu'à ce qu'on décide de supprimer ce qui est dans le pool (ou de le retenir s'ils souhaient le garder pour plus tard). On peut donc écrire ceci :
StringPrinter *getMyName() {
    return autorelease(new StringPrinter("Brunni"));
}
void function() {
    StringPrinter *name = getMyName();
    /* name n'appartient à personne mais existe toujours. On peut le retenir au besoin */
    name->print();
}
void main() {
    function();
    /* détruit ce qui ne sert vraiment plus à personne */
    drain_autorelease_pool();
}

Enfin, l'endroit typique pour "drainer" un pool est à chaque étape d'une boucle d'un jeu, après la gestion d'un événement de l'UI, etc. ainsi les valeurs de retour sont valables durant toute une hiérarchie d'appels réalisant une tâche, puis seront détruits si personne n'a décidé de les garder pour plus tard.
L'avantage de cette façon de gérer la mémoire est qu'elle est rapide (pas de GC) et très simple une fois qu'on l'a intégrée. Ca devient un automatisme, à tel point d'ailleurs qu'en Objective-C actuel on n'est plus obligé d'écrire ça, le compilo le fait pour nous et on écrit l'équivalent de ce qu'on ferait en java.

Donc voilà, je me suis amusé à reproduire ça en C++, d'abord en proposant une classe de base. C'est trivial, mais ça peut toujours aider comme base.// Fichier .h #include <cassert> class Object { int retainCount; int pendingReleases; public: Object() : retainCount(1), pendingReleases(0) {} virtual ~Object() {} void _retain() { retainCount++; } void _autorelease(); void _release() { if (--retainCount == 0) { // Mismatch (release called too many times) assert(!pendingReleases); delete this; } } static void drain_autorelease_pool(); }; template <class T> static inline T *retain(T *obj) { obj->_retain(); return (T*) obj; } template <class T> static inline T *autorelease(T *obj) { obj->_autorelease(); return (T*) obj; } static inline void release(Object *obj) { obj->_release(); } static inline void drain_autorelease_pool() { Object::drain_autorelease_pool(); }// Fichier .cpp #include <stdlib.h> #include <stdio.h> #include "objectruntime.h" static const unsigned initialPoolSize = 1024, poolSizeIncrement = 1024; static Object **pool = new Object*[initialPoolSize]; static unsigned poolCount, currentPoolSize = initialPoolSize; void Object::_autorelease() { // Not yet in pool => put it if (!pendingReleases) { // Need more elements for the pool? if (poolCount >= currentPoolSize) { currentPoolSize += poolSizeIncrement; pool = (Object**) realloc(pool, currentPoolSize * sizeof(Object**)); } // Store into the pool pool[poolCount++] = this; pendingReleases = 1; } else pendingReleases++; } void Object::drain_autorelease_pool() { Object **poolPtr = pool; printf("Draining pool: %d objects\n", poolCount); while (poolCount--) { Object *objToRelease = *poolPtr++; // Does not destroy it if (objToRelease->pendingReleases < objToRelease->retainCount) objToRelease->retainCount -= objToRelease->pendingReleases; else { // Mismatch (release called too many times) assert(objToRelease->pendingReleases == objToRelease->retainCount); delete objToRelease; } } }
Les fonctions en template sont là pour simplifier, on peut alors appeler MonObjet *obj = retain(new MonObjet()); par exemple. On peut aussi proposer les primitives retain, release etc. comme méthodes de la classe (il vous faut juste une classe qui étende Object et prenne le véritable type en template arg). Exemple :
class TestObject: public Object {
public:
	TestObject() { printf("Constructed!\n"); }
	~TestObject() { printf("Destroyed!\n"); }
	int getValue() { return 10; }
};
TestObject *createTestObject() {
	TestObject *retval = new TestObject();
	return autorelease(retval);
}
void useTestObject(TestObject *obj) {
	printf("Value: %d\n", obj->getValue());
}
int main(int argc, char *argv[]) {
	TestObject *ret = createTestObject();
	useTestObject(ret);
	drain_autorelease_pool();
	return 0;
}



Maintenant le challenge smile c'est de pouvoir faire ça sur n'importe quel objet, même en C pur (si vous rêviez de pouvoir retourner un char* et ne pas avoir à le désallouer par exemple). On peut faire ça assez simplement en fin de compte et avec un impact relativement faible.
Donc mon idée de base c'est d'avoir une table de correspondance entre l'adresse d'un objet et son retain count. Evidemment, on ne va pas faire une liste adresse -> retain count, qui serait longue à parcourir. Une table de hachage est plus efficace, et c'est assez facile à faire en extrayant quelques bits d'adresse pour l'indexer. Typiquement un objet @ 0x12345678 on peut imaginer extraire les avant-derniers bits (0x567) et utiliser ça comme index. Il suffit alors de voir s'il y a un objet qui correspond dans la table et si son adresse est exactement la même. Ainsi pour chaque entrée de la table on a une ou plusieurs entrées (liste chaînée). Dans mon cas j'ai choisi 4096 entrées, soit les bits 11 à 2 pour le hachage (32 bits, il faudrait prendre 12 à 3 en 64 bits).
Ainsi un retain incrémente le refcount et un release le décrémente. S'il tombe à zéro lors d'un release alors l'objet peut être supprimé. Pour l'autorelease pareil, mais il faut aussi associer au ref count un compteur "d'autoreleases en attente", et au moment de drainer le pool on va décrémenter le refcount de cette valeur et effacer l'objet s'il tombe à zéro (= plus personne ne s'en sert).
Cependant dans le cas de l'autorelease, la suppression est un peu complexe en C++ car il n'y a pas de pointeur vers le destructeur et ce dernier n'est pas garanti d'exister. Et on peut vouloir fonctionner en C pur aussi. Il faut alors fournir une façon d'effacer les objets qui puisse être invoquée de façon différée. Dans mon implém, j'ai créé une fonction avec template qui appelle delete, ainsi une version de cette fonction sera automatiquement créée pour chaque type d'objet qu'on inscrit au pool d'autorelease (il s'agit de 3 instructions, ce n'est pas grand chose). On peut aussi imaginer laisser au programmeur le soin fournir sa fonction, et dans le cas du C il pourrait passer systématiquement "free".

Voilà, maintenant le code pour ceux à qui ça peut servir : smile// unmanagedobjectruntime.h #ifndef UNMANAGEDOBJECTRUNTIME_H #define UNMANAGEDOBJECTRUNTIME_H // Private extern void _unmanaged_put_in_pool(void *obj); extern void _unmanaged_retain(void *obj); extern bool _unmanaged_release(void *obj); extern void _unmanaged_autorelease(void *obj, void (*deleter)(void*)); template <class T> static void _unmanaged_deleter(void *ptr) { delete static_cast<T*>(ptr); } // Public extern void unmanaged_autorelease(void *obj); extern void unmanaged_pool_drain(); template <class T> static inline T *unmanaged_put_in_pool(T *obj) { _unmanaged_put_in_pool(obj); return obj; } template <class T> static inline T *unmanaged_retain(T *obj) { _unmanaged_retain(obj); return obj; } template <class T> static inline T *unmanaged_autorelease(T *obj) { _unmanaged_autorelease(obj, &_unmanaged_deleter<T>); return obj; } template <class T> static inline void unmanaged_release(T *obj) { if (_unmanaged_release(obj)) delete obj; } #endif#include <cassert> #include <stdint.h> #include "unmanagedobjectruntime.h" // Power of two - 1 #define HASH_BIT_MASK 4095 // Number of LSB useless in an address (you may want to change to 3 for 64 bits...) #define REQUIRED_ADDY_SHIFT 2 struct RetainedObj { void *realAddress; void (*deleter)(void*); RetainedObj *nextIfAny; unsigned short retainCount, pendingReleases; RetainedObj() : realAddress(0), nextIfAny(0), retainCount(0), pendingReleases(0) {} RetainedObj(void *object) : realAddress(object), nextIfAny(0), retainCount(1), pendingReleases(0) {} }; static RetainedObj retainedObjList[HASH_BIT_MASK + 1]; void _unmanaged_put_in_pool(void *object) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash, *lastLine = line; // Make sure it doesn't exist yet assert(line->realAddress != object); while (lastLine->nextIfAny) { assert(lastLine->nextIfAny->realAddress != object); lastLine = lastLine->nextIfAny; } // Else, put in db. 1) room directly in line? if (!line->realAddress) { line->realAddress = object; line->retainCount = 1; } else { // 2) Line already exist, append to linked list lastLine->nextIfAny = new RetainedObj(object); } } void _unmanaged_retain(void *object) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // Find if already in db, and increment retain count in such case if (line->realAddress == object) { line->retainCount++; return; } // May be in linked list if (line->nextIfAny) { do { line = line->nextIfAny; if (line->realAddress == object) { line->retainCount++; return; } } while (line->nextIfAny); } // Shouldn't come here (should put in pool first) assert(0); } bool _unmanaged_release(void *object) { // Find in db unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // 1) the address of the object is in the base of the line if (line->realAddress == object) { if (--line->retainCount == 0) { // Remove the line and ask to remove the actual object line->realAddress = 0; return true; } return false; } // 2) the address is in the linked list RetainedObj *next = line->nextIfAny; while (next) { if (next->realAddress == object) { // Found object, remove from line if necessary if (--next->retainCount == 0) { // Remove from linked list line->nextIfAny = next->nextIfAny; delete next; return true; } return false; } line = next; next = line->nextIfAny; } // Shouldn't come here (not found in list) assert(0); return false; } void _unmanaged_autorelease(void *object, void (*deleter)(void*)) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // Find if already in db, and increment retain count in such case if (line->realAddress == object) { line->deleter = deleter; line->pendingReleases++; return; } // May be in linked list if (line->nextIfAny) { do { line = line->nextIfAny; if (line->realAddress == object) { line->deleter = deleter; line->pendingReleases++; return; } } while (line->nextIfAny); } // Shouldn't come here (should put in pool first) assert(0); } void unmanaged_pool_drain() { // Browse the list and delete objects where applicable for (unsigned i = 0; i < HASH_BIT_MASK + 1; i++) { RetainedObj *line = retainedObjList + i; if (line->realAddress) { assert(line->retainCount >= line->pendingReleases); line->retainCount -= line->pendingReleases; // Time to delete the object (use the deleter func) if (line->retainCount == 0) { assert(line->deleter && line->pendingReleases > 0); line->deleter(line->realAddress); } } // Same for the linked list RetainedObj *next = line->nextIfAny; while (next) { assert(next->retainCount >= next->pendingReleases); next->retainCount -= next->pendingReleases; if (next->retainCount == 0) { // Delete item and remove from linked list assert(next->deleter && next->pendingReleases > 0); next->deleter(next->realAddress); line->nextIfAny = next->nextIfAny; delete next; } line = next; next = line->nextIfAny; } } }

L'utilisation se fait alors comme ceci :
class TestObject {
public:
	TestObject() { printf("Constructed!\n"); }
	~TestObject() { printf("Destroyed!\n"); }
	int getValue() { return 10; }
};
class MyApp {
	TestObject *myObject;
public:
	MyApp(TestObject *obj) {
		myObject = unmanaged_retain(obj);
	}
	void testMethod() {
		printf("Value: %d\n", myObject->getValue());
	}
	~MyApp() {
		unmanaged_release(myObject);
	}
};
TestObject *getTestObject() {
	TestObject *retval = unmanaged_put_in_pool(new TestObject());
	return unmanaged_autorelease(retval);
}
int main(int argc, char** argv) {
	MyApp *app = new MyApp(getTestObject());
	app->testMethod();
	delete app;
	unmanaged_pool_drain();
	return 0;
}

On pourrait imaginer mettre automatiquement les objets dans le pool au premier autorelease ou retain, mais dans l'implém j'ai rendu l'appel à unmanaged_put_in_pool obligatoire pour commencer à gérer un objet non managé.