En comparant deux variantes de pointeurs - classique contre shared_ptr - j'ai été surpris par une augmentation significative de la vitesse d'exécution du programme. Pour tester l'algorithme d'insertion incrémentielle 2D Delaunay a été utilisé.
Paramètres du compilateur:
VS 2010 (version)/O2/MD/GL, W7 Prof, CPU 3.GHZ DualCore
Résultats:
shared_ptr (C++ 0x00):
N[points] t[sec]
100 000 6
200 000 11
300 000 16
900 000 36
Pointeurs:
N[points] t[sec]
100 000 0,5
200 000 1
300 000 2
900 000 4
Le temps d'exécution des versions shared_ptr est environ 10 fois plus long. Est-ce dû aux paramètres du compilateur ou à la mise en œuvre de C++ 0x00 shared_ptr est si lente?
VS2010 Profiler: Pour les pointeurs bruts, environ 60% du temps est consacré à la recherche heuristique du triangle contenant le point inséré (c'est OK, c'est un fait bien connu). Mais pour la version shared_ptr, environ 58% du temps est consacré à shared_ptr.reset () et seulement 10% est utilisé pour la recherche heuristique.
void DT2D::DT ( Node2DList *nl, HalfEdgesList *half_edges_dt, bool print )
{
// Create 2D Delaunay triangulation using incremental insertion method
unsigned int nodes_count_before = nl->size();
// Remove duplicit points
nl->removeDuplicitPoints();
// Get nodes count after deletion of duplicated points
unsigned int nodes_count_after = nl->size();
//Print info
std::cout << "> Starting DT, please wait... ";
std::cout << nodes_count_after << " points, " << ( nodes_count_before - nodes_count_after ) << " removed.";
// Are in triangulation more than three points
try
{
//There are at least 3 points
if ( nodes_count_after > 2 )
{
// Create simplex triangle
createSimplexTriangle ( nl, half_edges_dt );
// Increment nodes count
nodes_count_after += 3;
// Starting half Edge using for searching
HalfEdge *e_heuristic = ( *half_edges_dt ) [0];
// Insert all points into triangulation using incremental method
for ( unsigned int i = 3; i < nodes_count_after; i++ ) // Jump over simplex
{
DTInsertPoint ( ( *nl ) [i], &e_heuristic, half_edges_dt );
}
//Corect boundary triangles (swap edges in triangles adjacent to simplex triangles).
//They are legal due to DT, but not creating the convex hull )
correctBoundaryTriangles ( nl, half_edges_dt );
// Remove triangles having simplex points
removeSimplexTriangles ( nl, half_edges_dt );
}
//Print results
std::cout << " Completed." << std::endl;
}
void DT2D::DTInsertPoint ( Point2D *p, HalfEdge **e1, HalfEdgesList *half_edges_dt )
{
// One step of the Delaunay triangulation, incremental insertion by de Berg (2001)
short status = -1;
//Pointers
HalfEdge *e31 = NULL;
HalfEdge *e21 = NULL;
HalfEdge *e12 = NULL;
HalfEdge *e32 = NULL;
HalfEdge *e23 = NULL;
HalfEdge *e13 = NULL;
HalfEdge *e53 = NULL;
HalfEdge *e44 = NULL;
HalfEdge *e63 = NULL;
try
{
// Test, if point lies inside triangle
*e1 = LawsonOrientedWalk::findTriangleWalk ( p, &status, *e1, 0 );
if ( e1 != NULL )
{
// Edges inside triangle lies the point
HalfEdge *e2 = ( *e1 )->getNextEdge();
HalfEdge *e3 = e2->getNextEdge();
// Point lies inside the triangle
if ( status == 1 )
{
// Create first new triangle T1, twin edges set after creation
e31 = new HalfEdge ( p, *e1, NULL );
e21 = new HalfEdge ( e2->getPoint(), e31, NULL );
( *e1 )->setNextEdge ( e21 );
// Create second new triangle T2, twin edges set after creation
e12 = new HalfEdge ( p, e2, NULL );
e32 = new HalfEdge ( e3->getPoint(), e12, NULL );
e2->setNextEdge ( e32 );
// Create third new triangle T3, twin edges set after creation
e23 = new HalfEdge ( p, e3, NULL );
e13 = new HalfEdge ( ( *e1 )->getPoint(), e23, NULL );
e3->setNextEdge ( e13 );
// Set twin edges in T1, T2, T3
e12->setTwinEdge ( e21 );
e21->setTwinEdge ( e12 );
e13->setTwinEdge ( e31 );
e31->setTwinEdge ( e13 );
e23->setTwinEdge ( e32 );
e32->setTwinEdge ( e23 );
// Add new edges into list
half_edges_dt->Push_back ( e21 );
half_edges_dt->Push_back ( e12 );
half_edges_dt->Push_back ( e31 );
half_edges_dt->Push_back ( e13 );
half_edges_dt->Push_back ( e32 );
half_edges_dt->Push_back ( e23 );
// Legalize triangle T1
if ( ( *e1 )->getTwinEdge() != NULL )
{
legalizeTriangle ( p, *e1 );
}
// Legalize triangle T2
if ( e2->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e2 );
}
// Legalize triangle T3
if ( e3->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e3 );
}
}
// Point lies on the Edge of the triangle
else if ( status == 2 )
{
// Find adjacent triangle
HalfEdge *e4 = ( *e1 )->getTwinEdge();
HalfEdge *e5 = e4->getNextEdge();
HalfEdge *e6 = e5->getNextEdge();
// Create first new triangle T1, twin edges set after creation
e21 = new HalfEdge ( p, e3, NULL );
( *e1 )->setNextEdge ( e21 );
// Create second new triangle T2, OK
e12 = new HalfEdge ( p, e2, e4 );
e32 = new HalfEdge ( e3->getPoint(), e12, e21 );
e2->setNextEdge ( e32 );
// Create third new triangle T3, twin edges set after creation
e53 = new HalfEdge ( p, e6, NULL );
e4->setNextEdge ( e53 );
// Create fourth new triangle T4, OK
e44 = new HalfEdge ( p, e5, *e1 );
e63 = new HalfEdge ( e6->getPoint(), e44, e53 );
e5->setNextEdge ( e63 );
// Set twin edges in T1, T3
e21->setTwinEdge ( e32 );
( *e1 )->setTwinEdge ( e44 );
e53->setTwinEdge ( e63 );
e4->setTwinEdge ( e12 );
// Add new edges into list
half_edges_dt->Push_back ( e21 );
half_edges_dt->Push_back ( e12 );
half_edges_dt->Push_back ( e32 );
half_edges_dt->Push_back ( e53 );
half_edges_dt->Push_back ( e63 );
half_edges_dt->Push_back ( e44 );
// Legalize triangle T1
if ( e3->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e3 );
}
// Legalize triangle T4
if ( e5->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e5 );
}
// Legalize triangle T3
if ( e6->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e6 );
}
// Legalize triangle T2
if ( e2->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e2 );
}
}
}
}
//Throw exception
catch ( std::bad_alloc &e )
{
//Free memory
if ( e31 != NULL ) delete e31;
if ( e21 != NULL ) delete e21;
if ( e12 != NULL ) delete e12;
if ( e32 != NULL ) delete e32;
if ( e23 != NULL ) delete e23;
if ( e13 != NULL ) delete e13;
if ( e53 != NULL ) delete e53;
if ( e44 != NULL ) delete e44;
if ( e63 != NULL ) delete e63;
//Throw exception
throw ErrorBadAlloc ( "EErrorBadAlloc: ", "Delaunay triangulation: Can not create new triangles for inserted point p." );
}
//Throw exception
catch ( ErrorMathZeroDevision &e )
{
//Free memory
if ( e31 != NULL ) delete e31;
if ( e21 != NULL ) delete e21;
if ( e12 != NULL ) delete e12;
if ( e32 != NULL ) delete e32;
if ( e23 != NULL ) delete e23;
if ( e13 != NULL ) delete e13;
if ( e53 != NULL ) delete e53;
if ( e44 != NULL ) delete e44;
if ( e63 != NULL ) delete e63;
//Throw exception
throw ErrorBadAlloc ( "EErrorMathZeroDevision: ", "Delaunay triangulation: Can not create new triangles for inserted point p." );
}
}
Le code a été réécrit sans aucune optimisation ...
void DT2D::DTInsertPoint ( std::shared_ptr <Point2D> p, std::shared_ptr <HalfEdge> *e1, HalfEdgesList * half_edges_dt )
{
// One step of the Delaunay triangulation, incremental insertion by de Berg (2001)
short status = -1;
//Pointers
std::shared_ptr <HalfEdge> e31;
std::shared_ptr <HalfEdge> e21;
std::shared_ptr <HalfEdge> e12;
std::shared_ptr <HalfEdge> e32;
std::shared_ptr <HalfEdge> e23;
std::shared_ptr <HalfEdge> e13;
std::shared_ptr <HalfEdge> e53;
std::shared_ptr <HalfEdge> e44;
std::shared_ptr <HalfEdge> e63;
try
{
// Test, if point lies inside triangle
*e1 = LawsonOrientedWalk::findTriangleWalk ( p, &status, *e1, 0 );
if ( e1 != NULL )
{
// Edges inside triangle lies the point
std::shared_ptr <HalfEdge> e2((*e1 )->getNextEdge());
std::shared_ptr <HalfEdge> e3(e2->getNextEdge());
// Point lies inside the triangle
if ( status == 1 )
{
// Create first new triangle T1, twin edges set after creation
e31.reset( new HalfEdge ( p, *e1, NULL ));
e21.reset( new HalfEdge ( e2->getPoint(), e31, NULL ));
( *e1 )->setNextEdge ( e21 );
// Create second new triangle T2, twin edges set after creation
e12.reset( new HalfEdge ( p, e2, NULL ));
e32.reset( new HalfEdge ( e3->getPoint(), e12, NULL ));
e2->setNextEdge ( e32 );
// Create third new triangle T3, twin edges set after creation
e23.reset( new HalfEdge ( p, e3, NULL ));
e13.reset( new HalfEdge ( ( *e1 )->getPoint(), e23, NULL ));
e3->setNextEdge ( e13 );
// Set twin edges in T1, T2, T3
e12->setTwinEdge ( e21 );
e21->setTwinEdge ( e12 );
e13->setTwinEdge ( e31 );
e31->setTwinEdge ( e13 );
e23->setTwinEdge ( e32 );
e32->setTwinEdge ( e23 );
// Add new edges into list
half_edges_dt->Push_back ( e21 );
half_edges_dt->Push_back ( e12 );
half_edges_dt->Push_back ( e31 );
half_edges_dt->Push_back ( e13 );
half_edges_dt->Push_back ( e32 );
half_edges_dt->Push_back ( e23 );
// Legalize triangle T1
if ( ( *e1 )->getTwinEdge() != NULL )
{
legalizeTriangle ( p, *e1 );
}
// Legalize triangle T2
if ( e2->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e2 );
}
// Legalize triangle T3
if ( e3->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e3 );
}
}
// Point lies on the Edge of the triangle
else if ( status == 2 )
{
// Find adjacent triangle
std::shared_ptr <HalfEdge> e4 = ( *e1 )->getTwinEdge();
std::shared_ptr <HalfEdge> e5 = e4->getNextEdge();
std::shared_ptr <HalfEdge> e6 = e5->getNextEdge();
// Create first new triangle T1, twin edges set after creation
e21.reset(new HalfEdge ( p, e3, NULL ));
( *e1 )->setNextEdge ( e21 );
// Create second new triangle T2, OK
e12.reset(new HalfEdge ( p, e2, e4 ));
e32.reset(new HalfEdge ( e3->getPoint(), e12, e21 ));
e2->setNextEdge ( e32 );
// Create third new triangle T3, twin edges set after creation
e53.reset(new HalfEdge ( p, e6, NULL ));
e4->setNextEdge ( e53 );
// Create fourth new triangle T4, OK
e44.reset(new HalfEdge ( p, e5, *e1 ));
e63.reset(new HalfEdge ( e6->getPoint(), e44, e53 ));
e5->setNextEdge ( e63 );
// Set twin edges in T1, T3
e21->setTwinEdge ( e32 );
( *e1 )->setTwinEdge ( e44 );
e53->setTwinEdge ( e63 );
e4->setTwinEdge ( e12 );
// Add new edges into list
half_edges_dt->Push_back ( e21 );
half_edges_dt->Push_back ( e12 );
half_edges_dt->Push_back ( e32 );
half_edges_dt->Push_back ( e53 );
half_edges_dt->Push_back ( e63 );
half_edges_dt->Push_back ( e44 );
// Legalize triangle T1
if ( e3->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e3 );
}
// Legalize triangle T4
if ( e5->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e5 );
}
// Legalize triangle T3
if ( e6->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e6 );
}
// Legalize triangle T2
if ( e2->getTwinEdge() != NULL )
{
legalizeTriangle ( p, e2 );
}
}
}
}
//Throw exception
catch ( std::bad_alloc &e )
{
/*
//Free memory
if ( e31 != NULL ) delete e31;
if ( e21 != NULL ) delete e21;
if ( e12 != NULL ) delete e12;
if ( e32 != NULL ) delete e32;
if ( e23 != NULL ) delete e23;
if ( e13 != NULL ) delete e13;
if ( e53 != NULL ) delete e53;
if ( e44 != NULL ) delete e44;
if ( e63 != NULL ) delete e63;
*/
//Throw exception
throw ErrorBadAlloc ( "EErrorBadAlloc: ", "Delaunay triangulation: Can not create new triangles for inserted point p." );
}
//Throw exception
catch ( ErrorMathZeroDevision &e )
{
/*
//Free memory
if ( e31 != NULL ) delete e31;
if ( e21 != NULL ) delete e21;
if ( e12 != NULL ) delete e12;
if ( e32 != NULL ) delete e32;
if ( e23 != NULL ) delete e23;
if ( e13 != NULL ) delete e13;
if ( e53 != NULL ) delete e53;
if ( e44 != NULL ) delete e44;
if ( e63 != NULL ) delete e63;
*/
//Throw exception
throw ErrorBadAlloc ( "EErrorMathZeroDevision: ", "Delaunay triangulation: Can not create new triangles for inserted point p." );
}
}
Merci de votre aide...
J'ai remplacé le passage direct de tous les objets par le passage d'alias &. Les constructeurs de copie sont utilisés moins fréquemment qu'avant.
Tables mises à jour pour shared_ptr
shared_ptr (C++ 0x00) ancien:
N[points] t[sec]
100 000 6
200 000 11
300 000 16
900 000 36
shared_ptr (C++ 0x00) nouvelle version:
N[points] t[sec]
100 000 2
200 000 5
300 000 9
900 000 24
Il y a une amélioration considérable, mais la version shared_ptr est toujours 4 fois plus lente que celle du pointeur brut. Je crains que la vitesse d'exécution du programme ne puisse être augmentée de manière significative.
shared_ptr
Sont le type de pointeur le plus compliqué jamais créé:
Il y a 2 façons de les rendre plus rapides:
make_shared
pour les allouer , car (malheureusement ) le constructeur normal alloue deux blocs différents: un pour l'objet et un pour le compteur et le deleter.shared_ptr<T> const&
Mais il existe également de nombreuses façons de NE PAS les utiliser.
En regardant votre code, il semble que vous fassiez BEAUCOUP d'allocation de mémoire, et je ne peux m'empêcher de me demander si vous ne pouvez pas trouver une meilleure stratégie. Je dois admettre que je n'ai pas obtenu le chiffre complet, donc je vais peut-être me diriger directement vers un mur, mais ...
Le code est généralement beaucoup plus simple si vous avez un propriétaire pour chacun des objets. Par conséquent, shared_ptr
Devrait être une mesure de dernier recours, utilisée lorsque vous ne pouvez pas obtenir un seul propriétaire.
Quoi qu'il en soit, nous comparons des pommes et des oranges ici, le code d'origine est buggé. Vous vous occupez de deleting
de la mémoire (bonne) mais vous avez oublié que ces objets étaient également référencés à partir d'autres points du programme e1->setNextEdge(e21)
qui contient maintenant des pointeurs vers des objets détruits (dans un zone mémoire). Par conséquent, je suppose qu'en cas d'exception, vous effacez simplement la liste entière? (Ou en quelque sorte parier sur un comportement indéfini pour jouer à Nice)
Il est donc difficile de juger des performances car le premier ne se remet pas bien des exceptions tandis que le second le fait.
Enfin: Avez-vous pensé à utiliser intrusive_ptr ? Cela pourrait vous donner un coup de pouce (hehe) si vous ne les synchronisez pas (un seul thread) et vous éviteriez beaucoup de choses effectuées par le shared_ptr
Ainsi qu'un gain sur la localité de référence.
Je recommande toujours d'utiliser std :: shared_ptr <> au lieu de compter sur la gestion manuelle de la durée de vie de la mémoire. Cependant, la gestion automatique de la durée de vie coûte quelque chose mais n'est généralement pas significative.
Dans votre cas, vous avez remarqué que shared_ptr <> est important et comme certains l'ont dit, vous devez vous assurer de ne pas copier inutilement un pointeur partagé car cela force une addref/release.
Mais il y a une autre question en arrière-plan: avez-vous vraiment besoin de compter sur new/delete en premier lieu? new/delete utilise malloc/free qui n'est pas réglé pour les allocations de petits objets.
Une bibliothèque qui m'a beaucoup aidé auparavant est boost :: object_pool .
À un moment donné, j'ai voulu créer des graphiques très rapidement. Les nœuds et les bords sont naturellement alloués dynamiquement et j'en tire deux coûts.
boost: object_pool permet de réduire ces deux coûts au prix de ne pas être aussi général que malloc/free.
Donc, à titre d'exemple, disons que nous avons un nœud simple comme celui-ci:
struct node
{
node * left;
node * right;
};
Donc, au lieu d'un nœud d'allocation avec un nouveau, j'utilise boost :: object_pool. Mais boost :: object_pool suit également toutes les instances allouées avec lui, donc à la fin de mon calcul, j'ai détruit object_pool et je n'avais pas besoin de suivre chaque nœud, simplifiant ainsi mon code et améliorant les performances.
J'ai fait quelques tests de performances (j'ai écrit ma propre classe de pool juste pour le plaisir mais bool :: object_pool devrait donner les mêmes performances ou mieux).
10 000 000 nœuds créés et détruits
Donc, si boost :: object_pool fonctionne pour vous, cela pourrait aider à réduire considérablement la surcharge d'allocation de mémoire.
Par défaut, si vous créez vos pointeurs partagés de manière naïve (c'est-à-dire shared_ptr<type> p( new type )
) vous encourez deux allocations de mémoire, une pour l'objet réel et une allocation supplémentaire pour le compte de référence. Vous pouvez éviter l'allocation supplémentaire en utilisant le make_shared
modèle qui effectuera une instanciation unique pour l'objet et le nombre de références, puis construira sur place l'objet.
Le reste des coûts supplémentaires sont assez faibles par rapport au doublement des appels à malloc, comme l'incrémentation et la décrémentation du décompte (les deux opérations atomiques) et les tests de suppression. Si vous pouvez fournir du code sur la façon dont vous utilisez les pointeurs/pointeurs partagés, vous pourriez avoir une meilleure idée de ce qui se passe réellement dans le code.
Essayez-le en mode "release" et voyez si vous vous rapprochez des repères. Le mode débogage a tendance à activer de nombreuses assertions dans la STL, ce qui ralentit beaucoup de choses.
shared_ptr
sont sensiblement plus lents que les pointeurs bruts. C'est pourquoi ils ne devraient être utilisés que si vous besoin sémantique de propriété partagée.
Sinon, plusieurs autres types de pointeurs intelligents sont disponibles. scoped_ptr
et auto_ptr
(C++ 03) ou unique_ptr
(C++ 0x) ont tous deux leur utilité. Et souvent, la meilleure solution est de ne pas utiliser de pointeur d'aucune sorte, et d'écrire simplement votre propre classe RAII à la place.
UNE shared_ptr
doit incrémenter/décrémenter/lire le compteur de référence et, en fonction de l'implémentation et de la façon dont il est instancié, le compteur ref peut être alloué séparément, ce qui peut entraîner des échecs de cache potentiels. Et il doit accéder au compteur ref atomiquement, ce qui ajoute une surcharge supplémentaire.
Il est impossible de répondre à cela sans plus de données. Avez-vous profilé le code pour identifier avec précision la source du ralentissement dans la version shared_ptr? L'utilisation du conteneur ajoutera certainement des frais généraux, mais je serais surpris s'il le rend 10 fois plus lent.
VSTS dispose de bons outils de perf qui attribueront l'utilisation du processeur exactement si vous pouvez l'exécuter pendant environ 30 secondes. Si vous n'avez pas accès à VS Performance Tools ou à un autre jeu d'outils de profilage, exécutez le code shared_ptr dans le débogueur et entrez-le 10 ou 15 fois pour obtenir un échantillon de force brute de l'endroit où il passe tout son temps. C'est étonnamment et contre-intuitivement efficace, j'ai trouvé.
[EDIT] Ne passez pas votre shared_ptr par valeur dans cette variante du code - utilisez ref pour const. Si cette fonction est souvent appelée, cela aura un impact mesurable.
C'est lent car il utilise pour référence les instructions atomiques inc/dec, donc c'est horriblement lent. Si vous avez vraiment besoin de GC en C++, n'utilisez pas naïf RF GC et utilisez une stratégie RC plus développée, ou trace GC. http://www.hboehm.info/ gc / est agréable pour ne pas accélérer les tâches critiques (mais beaucoup mieux que les "pointeurs intelligents" RC naïfs).