[C++/Qt] Performance de l'utilisation de QSharedPointer
Présentation
Qt est un framework orienté objet écrit en C++ et permettant de faire des interfaces graphiques. Ce framework est utilisé par le projet KDE depuis ses débuts pour en faire un environnement de bureau très complet.
Qt fournit un ensemble de pointeur intelligent1 permettant de gérer plus facilement la mémoire. Le but est alors de ne plus avoir à supprimer des objets. La suppression se fera soit par un pointeur intelligent soit par le système de hiérarchie d'objet existant en Qt (l'objet père qui supprime l'ensemble des objets fils qui lui sont rattachés).
Qt propose l'ensemble des pointeurs intelligents suivants:
- QSharedDataPointer / QSharedData : ces deux classes utilisées ensemble permettent d'écrire un objet avec partage implicite. Cela signifie que l'objet fonctionnera comme la classe QString. Tant que l'objet est copié, passé en paramètre, .... l'objet n'est pas dupliqué (tous les objets pointes vers le même espace mémoire). Au moment où l'objet est modifié, l'objet est dupliqué. C'est ce qu'on appelle le COW2.
- QExplictlySharedDataPointer / QSharedData : QExplicitlySharedDataPointer est une variante de QSharedDataPointer. Ce pointeur intelligent, comme son nom l'indique, est détaché uniquement lorsque la méthode detach() est appelée explicitement. Cette classe permet de faire des objets qui fonctionnent comme des pointeurs mais qui sont utilisés sans la notion de pointeur (le *). La suppression des données partagées se fait donc quand tous les objets ne sont plus utilisés.
- QScopedPointer : Ce pointeur est le plus simple. Il permet de déclarer un pointeur sur le tas et s'occupe de la destruction de l'objet, lorsque le programme sort de la portée du bloc. Cela permet de ne plus se soucier de la libération du pointeur dans les cas d'erreur (exception, retour avant la fin de la fonction car le fichier n'a pu être ouvert, ...).
- QSharedPointer : Le pointeur dont on parlera dans la suite de ce billet. Il permet de partager non plus des données (comme le fait QSharedData) mais de partager un pointeur3. Nous allons voir dans la suite du billet, comment simplement utiliser ce pointeur, et les performances de ce pointeur par rapport à un pointeur standard.
Sommaire
Utilisation de QSharedPointer
A quoi sert-il ?
L'objet QSharedPointer
fait partie des pointeurs intelligents. Ces
pointeurs permettent de gérer automatiquement la libération de la
mémoire (plus besoin de faire delete ptr;
quand le pointeur n'est plus
utilisé) tout en restant utilisable comme un pointeur normal.
QSharedPointer
fonctionne par comptage de référence. Après la
déclaration, à chaque affectation, on augmente le compteur de référence,
lorsqu'on quitte la portée du bloc, on décrémente le compteur de
référence. QSharedPointer
détruit donc automatiquement le pointeur
quand il n'existe plus aucune référence vers ce pointeur.
QSharedPointer
vient donc comme une encapsulation de notre pointeur.
Comment l'utiliser ?
La déclaration d'un pointeur en C, se fait en écrivant MyObject*
. La
syntaxe en utilisant un QSharedPointer se fait en écrivant
QSharedPointer<MyObject>
. Par la suite dans le programme,
l'utilisation du pointeur QSharedObject
se fera de la même manière
qu'un pointeur C. (Avec l'opérateur ->
pour appeler un membre, une
méthode, ...) . Appelons dans la suite pointeur C, les pointeurs
standards et QSharedPointer
, le pointeur intelligent.
Afin d'éviter d'avoir un pointeur normal pouvant être supprimé à tout
moment dans l'application, lors de l'utilisation de QSharedPointer
, il
ne faut utiliser le pointeur C résultant du new
que pour la création
du QSharedPointer
. On peut donc directement créer le QSharedPointer
en utilisant le constructeur QSharedPointer ( T * ptr )
qui prend en
paramètre le pointeur C. C'est entre ces parenthèses que nous allons
créer le pointeur C4.
{ // Création du pointeur intelligent à partir d'un pointeur normal. QSharedPointer<MyObject> ptr(new MyObject()); // Utilisation du pointeur intelligent comme un pointeur normal. if (ptr) { ptr->setMembre(maValeur); } // Appel d'une méthode utilisant ce pointeur maMethode(ptr); }
Lorsque l'on quitte le bloc, si le comptage de référence tombe à 0, on
supprime le pointeur. A l'intérieur de maMethode()
le nombre de
référence sera passé à 2. Si la méthode utilise le pointeur mais ne
l'assigne nul part, le nombre de référence devrait être retombé à 1 et
donc ici sera décrémenté à 0.
Si par contre, maMethode()
fait des opérations d'assignation de ptr
et conserve une copie, le comptage ne tombera pas à 0 tant que l'objet
restera utilisé (assigné) ailleurs.
Regardons un exemple de maMethode()
:
void maMethode(QSharedPointer<MyObject> ptr) { ptr->setMembre2(maValeur);
Au début du bloc, ici le comptage de référence est à 2 et sera
décrémenté à la sortie de la méthode. On peut modifier les membres de
ptr
, et dans ce cas pas de changement du comptage de référence.
this->monPtr = ptr; }
Au contraire, on peut également l'assigner à un autre objet. Dans ce cas
le comptage de référence de cet objet passera à 3. A la sortie de la
méthode il sera décrémenté et passera alors à 2. L'objet ne sera pas
supprimé tant qu'on ne fera pas un this->monPtr.clear()
ou que this
ne sera pas détruit.
Si on veut garder une référence d'un pointeur mais qu'on ne souhaite pas
que celle-ci incrémente le nombre de référence du QSharedPointer
, il
est possible de créer un pointeur faible. Ce pointeur passe par
l'objet QWeakPointer
. Pour obtenir ce type de pointeur, il suffit de
faire :
QWeakPointer<MyObject> ptrW = ptr->toWeakRef ();
ptrW
n'incrémente donc pas le comptage de référence, cela signifie
donc que le pointeur peut être détruit même si un objet QWeakPointer
existe. Il sera alors possible de faire un ptrW.isNull()
pour savoir
si le pointeur est toujours valide. Si l'utilisateur a également besoin
d'avoir accès à un membre de l'objet, il pourra le transformer en
QSharedPointer
avant de l'utiliser sauf si le pointeur est null
.
QSharedPointer<MyObject> ptr2 = ptrW->toStrongRef (); if (ptr) { ptr->maMethodePtr(); }
Il faut tester que ptr2
est encore valide, car tant que la
transformation du pointeur faible vers le QSharedPointer
n'a pas
encore été fait, il est possible que le nombre de référence vers l'objet
soit tombé à 0 et qu'il ait été supprimé.
Comment utiliser this
Un des points peu pratique de l'utilisation de QSharedPointer
est que
le comptage de référence ne fonctionne pas si plusieurs QSharedPointer
pointent vers le même objet mais ont tous été créés à partir du pointeur
C. Prenons par exemple, le cas suivant :
MyObject * ptr = new MyObject(); QSharedPointer<MyObject> ptr1 = QSharedPointer(ptr); QSharedPointer<MyObject> ptr2 = QSharedPointer(ptr);
Le problème d'écrire ces lignes ainsi, et que pour ptr1
comme pour
ptr2
, l'objet n'est référencé qu'une fois. Ainsi le premier qui
tombera à 0 détruira l'objet, alors que l'autre pourrait encore
l'utiliser. Il faut donc écrire les choses comme suite :
QSharedPointer<MyObject> ptr1 = QSharedPointer(new MyObject()); QSharedPointer<MyObject> ptr2 = ptr1;
Ainsi ptr1
et ptr2
ont bien chacun connaissance de l'existence de
l'autre. Cela contraint donc à remplacer toutes les déclarations du type
MyObject*
par QSharedPointer<MyObject>
. Ceci est donc à faire dans
les paramètres des méthodes, dans les membres, dans la déclaration des
variables locales, ... . On ne peut donc plus utiliser le pointeur C
MyObject*
directement, mais seulement au travers de QSharedPointer
ou de QWeakPointer
.
Cela commence à poser problème lors de l'utilisation de this
dans un
objet. Imaginons une méthode d'un objet mettant à jour des membres fils
avec en paramètre le père. Nous aurions alors tendance à écrire ceci :
void Object2::setParent(QSharedPointer<MyObject> parent) { ... } .... void MyObject::setMember(Object2 * obj) { _membre = obj; if (obj) { obj->setParent(QSharedPointer<MyObject>(this)); } }
Ceci ne marchera pas car on créerait un nouvel objet QSharedPointer
commençant son comptage de référence à 1, alors que nous en avons déjà
au moins un autre pointant vers notre instance5. MyObject
pourrait alors être détruit alors qu'il est encore utilisé par
Object2
.
Pour éviter cela, il faut alors passer par un pointeur intelligent
this
. Pour cela nous allons utiliser deux choses :
- Un membre nommé
_this
de type pointeur intelligentQWeakPointer
, contenant une référence à l'objet lui même. (Nous n'utilisons pas un QSharedPointer, pour éviter une référence circulaire, voir le paragraphe suivant). - Une méthode statique utilisée pour la création (nous n'allons plus utiliser le constructeur, car à ce moment, il n'existe pas encore de QSharedPointer pointant vers notre objet).
Voici un exemple de comment écrire le constructeur maison :
class MyObject { public: static QSharedPointer<MyObject> create(QString parametre) { QSharedPointer<MyObject> ptr(new MyObject(parametre); ptr->_this = ptr.toWeakRef(); return ptr; } private: MyObject(QString parametre) { ... } QWeakPointer<MyObject> _this; };
Le constructeur devient alors privé (ou protégé si on a besoin de la
notion d'héritage) afin d'obliger l'utilisateur de la classe à utiliser
notre méthode de création. Dans notre nouvelle méthode de création
create
, qui est une méthode statique, nous allons créer le pointeur et
initialiser le QWeakPointer
de notre objet avec le pointeur
intelligent que nous venons de créer. Nous retournons un
QSharedPointer
. La méthode create
devient alors notre nouveau
constructeur, mais créant des instances d'objets de type
QSharedPointer<MyObject>
et non plus des instances d'objet
MyObject*
.
Notre méthode setMember()
peut alors être ré-écrite :
void MyObject::setMember(Object2 * obj) { _membre = obj; if (obj) { obj->setParent(_this.toStrongRef()); } }
Comment éviter les références circulaires
Le principe d'une référence circulaire est qu'un objet A référence l'objet B et l'objet B référence l'objet A.
Voici par exemple, un cas de référence circulaire :
class A { public: static create() { QSharedPointer<A> ptr(new A()); return ptr; } ~A() { } private: A() { b = B::create(); b->setA(_this); } QSharedPointer<B> b; QWeakPointer<A> _this; }; class B { public: static QSharedPointer<B> create() { QSharedPointer<B> ptr(new B()); return ptr; } void setA(QSharedPointer<A> b); QSharedPointer<A> getA(); private: QSharedPointer<A> a; };
Cela peut-être aussi le cas, si par exemple une instance d'objet C référence des instances d'objets fils C, qui possèdent eux-même un pointeur vers l'objet C père.
class C { public: static QSharedPointer<C> create() { QSharedPointer<C> ptr(new C()); _this = ptr.toWeakPtr(); return ptr; } ~C() { } void addChild(QSharedPointer<C> c) { _childs.append(c); c->setParent(_this); } void setParent(QSharedPointer<C> c); QSharedPointer<C> getParent(); private: C() { } QWeakPointer<C> _this; QSharedPointer<C> _parent; QList< QSharedPointer<C> > _childs; };
Dans ces cas là, on a :- L'objet A possède la référence vers l'objet B- L'objet B possède une référence vers l'objet A.- Même principe avec l'objet C
Dans ce cas, il restera alors toujours une référence vers A, et une vers B, même si plus aucune variable ne référence ces objets. Cette référence circulaire fait que l'objet ne sera jamais détruit même si on n'a plus besoin de l'objet.
Si on décide que l'objet A sera l'objet maitre (donc que sa destruction engendrera la destruction de l'objet B), on peut alors écrire les choses ainsi pour l'objet B :
class B { public: static QSharedPointer<B> create() { QSharedPointer<B> ptr(new B()); return ptr; } void setA(QWeakPointer<A> b); QWeakPointer<A> getA(); private: QWeakPointer<A> a; };
Dans ce cas, avec l'utilisation d'un QWeakPointer
, lorsque qu'il
n'existera plus de référence vers l'objet A, le pointeur faible a
sera mis à jour comme ne contenant plus de référence6.
L'instance de l'objet A sera réellement détruite. Il n'y aura alors plus
aucune référence vers l'objet B qui sera alors également
détruit7.
Utilisation dans les applications multi-thread.
L'utilisation de QSharedPointer
simplifie l'écriture des applications
multi-thread (les objets QSharedPointer
et QWeakPointer
sont
thread-safe).
Dans ces applications il n'y a alors plus besoin de se soucier si l'objet est en cours d'utilisation ailleurs dans l'application avant de le supprimer8. Lorsqu'un pointeur ne devient plus utilisé dans un thread donné, il ne sera détruit que s'il n'y a pas d'autres références dans d'autres threads de l'application9.
Avec l'utilisation de QWeakPointer
, un thread pourra tester
l'existence du pointeur avant d'effectuer une opération et pourra aviser
le cas échéant sans faire planter toute l'application10.
Utilisation d'un pool
Si la création et la destruction d'un objet est coûteux, il est envisageable de diminuer le coût de destruction et de création d'un thread en utilisant un Pool d'objet. Dans ce cas l'objet QQueue pourra être utilisé pour représenter notre Pool.
Lors de la demande de création, en utilisant notre méthode create
ci-dessus, on prend alors une valeur du pool (si disponible) et on la
retourne sous forme d'un QSharedPointer
.
QSharedPointer<MyObject> MyObject::create() { MyObject * c_ptr; if (_queue.size()) { c_ptr = _queue.dequeue(); } else { c_ptr = new MyObject(); } QSharedPointer<MyObject> ptr(c_ptr, ReturnToPool); ptr->_this = ptr.toWeakRef(); return ptr; }
Dans l'exemple ci-dessus11, on demande à la queue, qui doit
être une variable globale ou statique, un élément, et si ce n'est pas
possible, on crée un nouvel objet de type MyObject
(dont on suppose la
création coûteuse).
Lors de la création du QSharedPointer
on utilise alors le constructeur
QSharedPointer ( T * ptr, Deleter deleter )
sur lequel on définit une
méthode Deleter
nommée ReturnToPool
dont le but est de remettre les
objets en pool.
static void ReturnToPool(MyObject *obj) { if (_queue.size() < MAX_SIZE_QUEUE) { _queue.enqueue(obj); } else { delete obj; } }
Dans ce cas de retour au pool, si le pool est rempli, on détruit l'objet (pour éviter de consommer trop de mémoire), sinon on l'ajoute au pool. Dans ce cas, le pool est agrandi au fur et à mesure des besoins, jusqu'à une taille limite.
Bien sûr il faut que la performance de l'utilisation d'un pool soit plus intéressante que celle de la création de l'objet et de son initialisation.
Attention : Ce point ne fonctionne, par contre, pas si
l'objet (MyObject
) est un descendant de QObject
. En effet QObject
garde une référence du QSharedPointer
en mémoire et lors de la
réutilisation du QObject
une erreur indique que l'objet n'a pas été
détruit et est déjà utilisé par un QSharedPointer
. On n'a pas le
problème avec std::tr1::shared_ptr
Benchmark
Le but du benchmark est de se faire une idée sur les performances d'une
application utilisant des QSharedPointer
à la place des pointeurs
normaux. Attention, ce bench ne prend pas en compte le besoin potentiel
de Mutex, de comptage de référence manuel, ... dans les applications
multi-thread qui pourrait être nécessaire pour ne pas supprimer le
pointeur si besoin.
Dans ce test nous allons tester également (en comparaison), le pointeur intelligent du C++0x12.
Nous allons donc tester les opérations courantes de création, destruction, modification, affectation.
Code source
Le code source est disponible, attaché au billet. Dans la suite du
billet, seuls les morceaux intéressants du benchmark seront décris. Le
benchmark utilise QTest. Nous avons créé un objet bidon ObjetTest
qui
dans le constructeur allouera un pointeur et remplira une liste, et le
destructeur supprime ce pointeur (et forcément la liste).
Pour que chaque test soit indépendant, le jeu de test sera initialisé avant le début de chaque QBENCHMARK et détruit à la fin du bloc. Nous aurons quatre méthodes :
- Allocation
- Modification
- Affectation
- Nettoyage
Pour chaque test nous allons faire le test avec
- un pointeur C standard
- le pointeur
QSharedPointer
de Qt - le pointeur
std::tr1::shared_ptr
de C++0x
Pour le test d'allocation et le test de nettoyage, nous allons également utiliser l'optimisation possible, vu ci-dessus, d'un Pool d'objet. Nous allons faire le test avec :
- le pointeur
QSharedPointer
de Qt - le pointeur
std::tr1::shared_ptr
de C++0x
Le jeu de test
Test de l'allocation
La création du pointeur en utilisant QSharedPointer
instancie le
pointeur ainsi que le QSharedPointer
. Le temps d'exécution est donc
potentiellement deux fois plus long (voir le benchmark à la fin de ce
billet).
Pour la création du pool, nous allons utiliser une méthode qui créera le pointeur s'il n'est pas dans le pool, et sinon prendra le pointeur du pool. Dans notre cas de test, il y aura toujours une valeur dans le pool, que l'on aura rempli au préalable.
La méthode createFromPool()
et createFromBoostPool()
est
sensiblement identique :
QSharedPointer<ObjetTest> createFromPool() { ObjetTest * c_ptr; if (_queue.size()) { c_ptr = _queue.dequeue(); } else { c_ptr = new ObjetTest(); } QSharedPointer<ObjetTest> ptr(c_ptr, returnToPool); return ptr; }
Méthode | Code |
---|---|
C Pointer | ObjetTest* ptr = new ObjetTest(); |
Qt Smart Pointer | QSharedPointer<ObjetTest> ptr(new ObjetTest()); |
Qt Smart Pointer as Pool | QSharedPointer<ObjetTest> ptr = createFromPool (); |
C++0x Smart Pointer | std::tr1::shared_ptr<ObjetTest> ptr(new ObjetTest()); |
C++0x Smart Pointer as Pool | std::tr1::shared_ptr<ObjetTest> ptr = createFromBoostPool (); |
Test de Modification d'une donnée
Pour la modification d'une donnée, on génère un nombre aléatoire que l'on va stocker (toujours le même pour chaque test, cela n'a pas d'importance). La génération du nombre aléatoire se fait en dehors du bloc, pour éviter de polluer le test avec le calcul d'un nombre aléatoire. Ici il n'y a pas création d'affectation du pointeur, juste une affectation d'une valeur dans le contenu du pointeur. La syntaxe pour le C, et pour le pointeur intelligent est identique.
Méthode | Code |
---|---|
C Pointer / Qt Smart Pointer / C++0x Smart Pointer | obj->value = random_number; |
Test d'affectation
Pour l'affectation nous allons créer une nouvelle variable qui pointera sur le même pointeur, et sur lequel on fera une modification. La création d'un pointeur peut arriver par exemple lors du passage du pointeur à une fonction, ou lors de la déclaration d'une variable devant contenir la même valeur. Cette déclaration supplémentaire a peu d'impact pour un pointeur C mais pour un pointeur intelligent oblige la création d'un objet, et l'incrément d'un nombre d'instance (qu'on décrémente ici dans la même boucle).
Méthode | Code |
---|---|
C Pointer | ObjetTest * obj2 = obj; |
obj2->value = random_number; |
|
Qt Smart Pointer | QSharedPointer<ObjetTest> obj2 = obj; |
obj2->value = random_number; |
|
C++0x Smart Pointer | std::tr1::shared_ptr<ObjetTest> obj2 = obj; |
obj2->value = random_number; |
Test de destruction
Pour ce test, nous allons initialiser une liste de pointeur, et pour le
benchmark, nous allons supprimer un à un chaque élément de la liste.La
destruction du pointeur en C se fait par un delete
. Pour le pointeur
''intelligent', il n'y a pas de destruction explicite. Nous allons juste
supprimer le pointeur de la liste, le pointeur sera alors
automatiquement détruit car il n'y aura plus de référence vers ce
pointeur.
Pour le cas de test utilisant la notion du Pool, on aura créé le
pointeur avec le delete returnToPool()
:
void returnToPool(ObjetTest *obj) { _queue.enqueue(obj); }
Cette méthode ne fait pas de réelle destruction, mais juste un ajout de l'objet au pool.
Méthode | Code |
---|---|
C Pointer | delete c_ptr_list.at(0); |
c_ptr_list.removeFirst (); |
|
Qt Smart Pointer | smart_ptr_list.removeFirst (); |
Qt Smart Pointer as Pool | |
C++0x Smart Pointer | |
C++0x Smart Pointer as Pool |
Résultat du test
Le test a été fait en utilisant la version 4.6.3 de Qt. Test effectué pour 5 000 000 itérations.
Pointeur C | Pointeur Qt | Pointeur C++0x | Pool en utilisant QSharedPointer | Pool en utilisant std::tr1::shared_ptr | |
---|---|---|---|---|---|
Allocation |
0.0004275 msec | 0.0007692 msec | 0.0006604 msec | 0.0002590 msec | 0.0002286 msec |
Modification |
0.000010 msec | 0.000012 msec | 0.000012 msec | ||
Affectation |
0.000010 msec | 0.0000386 msec | 0.0000230 msec | ||
Destruction |
0.000190 msec | 0.0003161 msec | 0.0003359 msec | 0.0004003 msec | 0.0003601 msec |
Conclusion que l'on peut en tirer, le pointeur C est ce qu'il y a de plus rapide à partir du moment où on fait de l'allocation de l'affectation ou de la destruction. Par contre il n'apporte pas la souplesse qu'apporte les pointeurs intelligents entre autre pour les applications multi-threadé.
On remarque que le pointeur C++0x est plus rapide pour la création, mais apparemment plus lent en destruction. Il est également possible avec le pool de gagner en performance (surtout en création). Par contre le coût de destruction de l'objet n'est pas encore assez fort pour y gagner en utilisant le pool.
Ensuite il est important de se faire son propre jugement selon ses besoins. Si besoin le source est attaché, vous pouvez faire vos propres tests.
Source du test
Vous pouvez trouver les sources du test au lien suivant.
-
en anglais : smart-pointer ↩
-
COW = Copy On Write ↩
-
Ce pointeur est l'équivalent du pointeur intelligent [boost::shared_ptr][] du projet Boost. Boost est une librairie qui ajoute beaucoup de facilité pour les programmes en C++, comme par exemple les smart-pointer dont certains seront inclus dans C++0x, ou de la boucle
std::for_each
. ↩ -
Si à un moment donné il faut utiliser le pointeur C pour une raison quelconque, on peut utiliser
ptr.data()
mais il faut s'assurer que le pointeur ne sera pas détruit en déclarant unQSharedPointer
dans le même bloc utilisant le pointeur C. LeQSharedPointer
ne devra être détruit qu'après utilisation du pointeur C. Ceci peut être fait dans certain cas pour des raisons de performance. ↩ -
Sinon nous ne serions plus là pour lancer la méthode ↩
-
Le
QWeakPointeur
ne gardant pas d'instance d'objet, car il n'incrémente pas le compteur de référence ↩ -
s'il n'existe pas de référence vers l'objet B ailleurs dans l'application ↩
-
Attention quand même,
QSharedPointer
protège le pointeur mais pas le contenu ↩ -
d'autres threads pouvant inclure le thread principal ↩
-
Si l'objet est supprimé,
QWeakPointer
, sera alors remis ànull
↩ -
Dans le code en question, il faudrait ajouter la notion de mutex autour de la gestion de la queue, en cas de création parallèle. ↩
-
Le pointeur intelligent
shared_ptr
de C++0x à pour origine le pointeur Boost ↩