J'ai toujours pensé que c'était la sagesse générale que std::vector
est "implémenté comme un tableau", bla bla bla. Aujourd'hui, je suis allé le tester et cela ne semble pas être le cas:
Voici quelques résultats de test:
UseArray completed in 2.619 seconds
UseVector completed in 9.284 seconds
UseVectorPushBack completed in 14.669 seconds
The whole thing completed in 26.591 seconds
C'est environ 3 à 4 fois plus lent! Ne justifie pas vraiment les commentaires "vector
peut être plus lent pour quelques nanosecs".
Et le code que j'ai utilisé:
#include <cstdlib>
#include <vector>
#include <iostream>
#include <string>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>
class TestTimer
{
public:
TestTimer(const std::string & name) : name(name),
start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
{
}
~TestTimer()
{
using namespace std;
using namespace boost;
posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
posix_time::time_duration d = now - start;
cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
" seconds" << endl;
}
private:
std::string name;
boost::posix_time::ptime start;
};
struct Pixel
{
Pixel()
{
}
Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
{
}
unsigned char r, g, b;
};
void UseVector()
{
TestTimer t("UseVector");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel> pixels;
pixels.resize(dimension * dimension);
for(int i = 0; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
}
}
void UseVectorPushBack()
{
TestTimer t("UseVectorPushBack");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel> pixels;
pixels.reserve(dimension * dimension);
for(int i = 0; i < dimension * dimension; ++i)
pixels.Push_back(Pixel(255, 0, 0));
}
}
void UseArray()
{
TestTimer t("UseArray");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);
for(int i = 0 ; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
free(pixels);
}
}
int main()
{
TestTimer t1("The whole thing");
UseArray();
UseVector();
UseVectorPushBack();
return 0;
}
Est-ce que je le fais mal ou quelque chose? Ou est-ce que je viens de briser ce mythe de la performance?
J'utilise le mode Release dans Visual Studio 2005 .
Dans Visual C++ , #define _SECURE_SCL 0
réduit de UseVector
de moitié (le ramenant à 4 secondes). C'est vraiment énorme, IMO.
En utilisant ce qui suit:
g ++ -O3 Time.cpp -I <MyBoost>
./ a.out
UseArray terminé en 2.196 secondes
UseVector terminé en 4.412 secondes
UseVectorPushBack terminé en 8.017 secondes
Le tout terminé en 14.626 secondes
Le tableau est donc deux fois plus rapide que le vecteur.
Mais après avoir examiné le code plus en détail, cela est prévu; lorsque vous rencontrez le vecteur deux fois et le tableau une seule fois. Remarque: lorsque vous resize()
le vecteur, vous allouez non seulement de la mémoire, mais vous parcourez également le vecteur et appelez le constructeur de chaque membre.
Réarrangez légèrement le code pour que le vecteur n'initialise chaque objet qu'une seule fois:
std::vector<Pixel> pixels(dimensions * dimensions, Pixel(255,0,0));
Maintenant, refaites le même timing:
g ++ -O3 Time.cpp -I <MyBoost>
./ a.out
UseVector terminé en 2.216 secondes
Les performances du vecteur ne sont que légèrement inférieures à celles du tableau. OMI, cette différence est insignifiante et pourrait être causée par un tas de choses qui ne sont pas associées au test.
Je prendrais également en compte le fait que vous n’initialisez/détruisez pas correctement l’objet Pixel dans la méthode UseArrray()
car aucun constructeur/destructeur n’est appelé (cela peut ne pas poser de problème pour cette classe simple, mais rien de plus complexe (c'est-à-dire avec des pointeurs ou des membres avec des pointeurs) posera problème.
Excellente question. Je suis arrivé ici en espérant trouver une solution simple pour accélérer les tests vectoriels. Cela n'a pas fonctionné comme prévu!
L'optimisation aide, mais ce n'est pas suffisant. Avec l'optimisation, je constate toujours une différence de performances 2X entre UseArray et UseVector. Fait intéressant, UseVector était nettement plus lent que UseVectorPushBack sans optimisation.
# g++ -Wall -Wextra -pedantic -o vector vector.cpp
# ./vector
UseArray completed in 20.68 seconds
UseVector completed in 120.509 seconds
UseVectorPushBack completed in 37.654 seconds
The whole thing completed in 178.845 seconds
# g++ -Wall -Wextra -pedantic -O3 -o vector vector.cpp
# ./vector
UseArray completed in 3.09 seconds
UseVector completed in 6.09 seconds
UseVectorPushBack completed in 9.847 seconds
The whole thing completed in 19.028 seconds
J'ai essayé de changer malloc()
en new[]
Dans UseArray pour que les objets soient construits. Et passer d'une assignation de champ individuelle à une assignation d'instance de pixel. Oh, et renommer la variable de boucle interne en j
.
void UseArray()
{
TestTimer t("UseArray");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
// Same speed as malloc().
Pixel * pixels = new Pixel[dimension * dimension];
for(int j = 0 ; j < dimension * dimension; ++j)
pixels[j] = Pixel(255, 0, 0);
delete[] pixels;
}
}
Étonnamment (pour moi), aucun de ces changements n'a fait la moindre différence. Pas même le changement de new[]
Qui construira par défaut tous les pixels. Il semble que gcc puisse optimiser les appels de constructeur par défaut en utilisant new[]
, Mais pas en utilisant vector
.
J'ai également essayé de me débarrasser de la triple recherche operator[]
Et de mettre en cache la référence à pixels[j]
. Cela a en fait ralenti UseVector! Oops.
for(int j = 0; j < dimension * dimension; ++j)
{
// Slower than accessing pixels[j] three times.
Pixel &pixel = pixels[j];
pixel.r = 255;
pixel.g = 0;
pixel.b = 0;
}
# ./vector
UseArray completed in 3.226 seconds
UseVector completed in 7.54 seconds
UseVectorPushBack completed in 9.859 seconds
The whole thing completed in 20.626 seconds
Qu'en est-il de supprimer complètement les constructeurs? Alors peut-être que gcc pourra optimiser la construction de tous les objets lors de la création des vecteurs. Que se passe-t-il si nous changeons Pixel en:
struct Pixel
{
unsigned char r, g, b;
};
Résultat: environ 10% plus rapide. Encore plus lent qu'un tableau. Hm.
# ./vector
UseArray completed in 3.239 seconds
UseVector completed in 5.567 seconds
Pourquoi ne pas utiliser un vector<Pixel>::iterator
Au lieu d'un index de boucle?
for (std::vector<Pixel>::iterator j = pixels.begin(); j != pixels.end(); ++j)
{
j->r = 255;
j->g = 0;
j->b = 0;
}
Résultat:
# ./vector
UseArray completed in 3.264 seconds
UseVector completed in 5.443 seconds
Non, pas différent. Au moins, ce n'est pas plus lent. Je pensais que cela aurait une performance similaire à la n ° 2 où j'ai utilisé une référence Pixel&
.
Même si certains cookies intelligents déterminent comment rendre le vecteur en boucle aussi rapide que celui du tableau, cela ne parle pas bien du comportement par défaut de std::vector
. C’est beaucoup pour le compilateur qui est assez intelligent pour optimiser tout le C++ et rendre les conteneurs STL aussi rapides que des tableaux bruts.
L'essentiel est que le compilateur ne peut pas optimiser les appels de constructeur par défaut sans opération lorsqu'il utilise std::vector
. Si vous utilisez plain new[]
, Cela les optimise parfaitement. Mais pas avec std::vector
. Même si vous pouvez réécrire votre code pour éliminer les appels des constructeurs qui défient le mantra: "Le compilateur est plus intelligent que vous. Le STL est aussi rapide que le simple C. Ne vous en faites pas."
C'est une question ancienne mais populaire.
À ce stade, de nombreux programmeurs travailleront en C++ 11. Et en C++ 11, le code de l'OP tel qu'il est écrit s'exécute aussi rapidement pour UseArray
ou UseVector
.
UseVector completed in 3.74482 seconds
UseArray completed in 3.70414 seconds
Le problème fondamental était que, tandis que votre structure Pixel
était non initialisée, std::vector<T>::resize( size_t, T const&=T() )
prend une valeur par défaut construite Pixel
et que la copie . Le compilateur n'a pas remarqué qu'il lui était demandé de copier des données non initialisées. Il a donc effectué la copie.
En C++ 11, std::vector<T>::resize
A deux surcharges. Le premier est std::vector<T>::resize(size_t)
, le second est std::vector<T>::resize(size_t, T const&)
. Cela signifie que lorsque vous appelez resize
sans second argument, les constructions par défaut sont simples et le compilateur est suffisamment intelligent pour se rendre compte que la construction par défaut ne fait rien, de sorte qu’elle ignore le passage sur le tampon.
(Les deux surcharges ont été ajoutées pour gérer les types mobiles, constructibles et non copiables - l'amélioration des performances lorsque vous travaillez sur des données non initialisées est un bonus).
La solution Push_back
Effectue également une vérification fencepost, ce qui la ralentit et reste donc plus lente que la version malloc
.
exemple en direct (j'ai également remplacé le temporisateur par chrono::high_resolution_clock
).
Notez que si vous avez une structure qui nécessite généralement une initialisation, mais que vous souhaitez gérer après la croissance de votre tampon, vous pouvez le faire avec un allocateur personnalisé std::vector
. Si vous voulez ensuite le déplacer dans un std::vector
Plus normal, je pense qu'une utilisation prudente de allocator_traits
Et un remplacement de ==
Pourraient le retirer, mais je ne suis pas sûr.
Pour être juste, vous ne pouvez pas comparer une implémentation C++ à une implémentation C, comme j'appellerais votre version malloc. malloc ne crée pas d'objets, il n'alloue que de la mémoire brute. Le fait que vous traitiez alors cette mémoire comme des objets sans appeler le constructeur est un C++ médiocre (éventuellement non valide - je laisserai cela aux avocats spécialistes du langage).
Cela dit, il suffit de changer le malloc en new Pixel[dimensions*dimensions]
et libre de delete [] pixels
ne fait pas beaucoup de différence avec la simple implémentation de Pixel que vous avez. Voici les résultats sur ma boîte (E6600, 64 bits):
UseArray completed in 0.269 seconds
UseVector completed in 1.665 seconds
UseVectorPushBack completed in 7.309 seconds
The whole thing completed in 9.244 seconds
Mais avec un léger changement, les tables tournent:
struct Pixel
{
Pixel();
Pixel(unsigned char r, unsigned char g, unsigned char b);
unsigned char r, g, b;
};
#include "Pixel.h"
Pixel::Pixel() {}
Pixel::Pixel(unsigned char r, unsigned char g, unsigned char b)
: r(r), g(g), b(b) {}
#include "Pixel.h"
[rest of test harness without class Pixel]
[UseArray now uses new/delete not malloc/free]
Compilé de cette façon:
$ g++ -O3 -c -o Pixel.o Pixel.cc
$ g++ -O3 -c -o main.o main.cc
$ g++ -o main main.o Pixel.o
nous obtenons des résultats très différents:
UseArray completed in 2.78 seconds
UseVector completed in 1.651 seconds
UseVectorPushBack completed in 7.826 seconds
The whole thing completed in 12.258 seconds
Avec un constructeur non-intégré pour Pixel, std :: vector bat maintenant un tableau brut.
Il semblerait que la complexité de l'allocation via std :: vector et std: allocator soit trop complexe pour être optimisée aussi efficacement qu'un simple new Pixel[n]
. Cependant, nous pouvons voir que le problème est simplement lié à l'allocation et non au vecteur d'accès en modifiant quelques fonctions de test pour créer le vecteur/tableau une fois en le déplaçant en dehors de la boucle:
void UseVector()
{
TestTimer t("UseVector");
int dimension = 999;
std::vector<Pixel> pixels;
pixels.resize(dimension * dimension);
for(int i = 0; i < 1000; ++i)
{
for(int i = 0; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
}
}
et
void UseArray()
{
TestTimer t("UseArray");
int dimension = 999;
Pixel * pixels = new Pixel[dimension * dimension];
for(int i = 0; i < 1000; ++i)
{
for(int i = 0 ; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
}
delete [] pixels;
}
Nous obtenons ces résultats maintenant:
UseArray completed in 0.254 seconds
UseVector completed in 0.249 seconds
UseVectorPushBack completed in 7.298 seconds
The whole thing completed in 7.802 seconds
Ce que nous pouvons en apprendre, c’est que std :: vector est comparable à un tableau brut d’accès, mais si vous devez créer et supprimer plusieurs fois le vecteur/tableau, la création d’un objet complexe prendra plus de temps que la création d’un tableau simple. lorsque le constructeur de l'élément n'est pas en ligne. Je ne pense pas que cela soit très surprenant.
Essayez avec ceci:
void UseVectorCtor()
{
TestTimer t("UseConstructor");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
}
}
J'obtiens presque exactement les mêmes performances qu'avec un tableau.
Le problème avec vector
est que c'est un outil beaucoup plus général qu'un tableau. Et cela signifie que vous devez considérer comment vous l'utilisez. Il peut être utilisé de nombreuses manières différentes, fournissant des fonctionnalités qu’un tableau n’a même pas. Et si vous l'utilisez "mal" pour vos besoins, vous vous exposez à beaucoup de frais généraux, mais si vous l'utilisez correctement, il s'agit généralement d'une structure de données à zéro frais généraux. Dans ce cas, le problème est que vous avez initialisé séparément le vecteur (ce qui a pour effet d'appeler tous les éléments leur ctor par défaut), puis d'écraser individuellement chaque élément avec la valeur correcte. C’est beaucoup plus difficile pour le compilateur d’optimiser que lorsque vous faites la même chose avec un tableau. C'est pourquoi le vecteur fournit un constructeur qui vous permet de faire exactement cela: initialiser N
éléments avec la valeur X
.
Et lorsque vous l'utilisez, le vecteur est aussi rapide qu'un tableau.
Donc non, vous n'avez pas brisé le mythe de la performance. Mais vous avez montré que ce n’est vrai que si vous utilisez le vecteur de manière optimale, ce qui est également un très bon point. :)
Du bon côté des choses, c’est vraiment l’utilisation la plus simple qui s’avère la plus rapide. Si vous opposez mon extrait de code (une seule ligne) à la réponse de John Kugelman, contenant des tas de modifications et d'optimisations, qui n'éliminent toujours pas la différence de performances, il est assez clair que vector
est intelligemment conçu après tout. Vous n'avez pas à sauter dans les cerceaux pour obtenir une vitesse égale à celle d'un tableau. Au contraire, vous devez utiliser la solution la plus simple possible.
Ce n’était pas une comparaison juste lorsque j’ai examiné votre code pour la première fois; Je pensais vraiment que vous ne compariez pas des pommes avec des pommes. Alors j'ai pensé, appelons les constructeurs et les destructeurs à tous les tests; et ensuite comparer.
const size_t dimension = 1000;
void UseArray() {
TestTimer t("UseArray");
for(size_t j = 0; j < dimension; ++j) {
Pixel* pixels = new Pixel[dimension * dimension];
for(size_t i = 0 ; i < dimension * dimension; ++i) {
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = (unsigned char) (i % 255);
}
delete[] pixels;
}
}
void UseVector() {
TestTimer t("UseVector");
for(size_t j = 0; j < dimension; ++j) {
std::vector<Pixel> pixels(dimension * dimension);
for(size_t i = 0; i < dimension * dimension; ++i) {
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = (unsigned char) (i % 255);
}
}
}
int main() {
TestTimer t1("The whole thing");
UseArray();
UseVector();
return 0;
}
Je pensais qu'avec cette configuration, ils devraient être exactement identiques. Il s'est avéré que j'avais tort.
UseArray completed in 3.06 seconds
UseVector completed in 4.087 seconds
The whole thing completed in 10.14 seconds
Alors pourquoi cette perte de performance de 30% s'est-elle produite? La STL contient tout dans les en-têtes; le compilateur aurait donc dû comprendre tout ce qui était nécessaire.
Mes pensées étaient que c'est dans la façon dont la boucle initialise toutes les valeurs au constructeur par défaut. J'ai donc effectué un test:
class Tester {
public:
static int count;
static int count2;
Tester() { count++; }
Tester(const Tester&) { count2++; }
};
int Tester::count = 0;
int Tester::count2 = 0;
int main() {
std::vector<Tester> myvec(300);
printf("Default Constructed: %i\nCopy Constructed: %i\n", Tester::count, Tester::count2);
return 0;
}
Les résultats étaient comme je le suspectais:
Default Constructed: 1
Copy Constructed: 300
Ceci est clairement la source du ralentissement, le fait que le vecteur utilise le constructeur de copie pour initialiser les éléments à partir d'un objet construit par défaut.
Cela signifie que l'ordre de pseudo-opération suivant est en cours lors de la construction du vecteur:
Pixel pixel;
for (auto i = 0; i < N; ++i) vector[i] = pixel;
Qui, en raison du constructeur de copie implicite créé par le compilateur, est développé comme suit:
Pixel pixel;
for (auto i = 0; i < N; ++i) {
vector[i].r = pixel.r;
vector[i].g = pixel.g;
vector[i].b = pixel.b;
}
Ainsi, la valeur par défaut Pixel
reste non initialisée, tandis que les autres valeurs sont initialisées avec les valeurs par défaut Pixel
non initialisées.
Par rapport à la situation alternative avec New[]
/Delete[]
:
int main() {
Tester* myvec = new Tester[300];
printf("Default Constructed: %i\nCopy Constructed:%i\n", Tester::count, Tester::count2);
delete[] myvec;
return 0;
}
Default Constructed: 300
Copy Constructed: 0
Ils sont tous laissés à leurs valeurs non initialisées, et sans la double itération sur la séquence.
Armés de ces informations, comment pouvons-nous les tester? Essayons de remplacer le constructeur de copie implicite.
Pixel(const Pixel&) {}
Et les résultats?
UseArray completed in 2.617 seconds
UseVector completed in 2.682 seconds
The whole thing completed in 5.301 seconds
En résumé, si vous créez très souvent des centaines de vecteurs: repensez votre algorithme.
Dans tous les cas, l’implémentation [~ # ~] stl [~ # ~] n’est pas plus lente pour une raison inconnue, elle fait exactement ce que vous voulez. demander; en espérant que vous savez mieux.
Essayez de désactiver itérateurs vérifiés et de construire en mode release. Vous ne devriez pas voir beaucoup de différence de performance.
Le STL de GNU (et autres), étant donné vector<T>(n)
, construit par défaut un objet prototype T()
- le compilateur optimisera le constructeur vide - mais ensuite une copie de tout ce qui est arrivé se trouve dans le Les adresses mémoire maintenant réservées à l'objet sont prises par la __uninitialized_fill_n_aux
, qui met en boucle les copies de cet objet en tant que valeurs par défaut dans le vecteur. Ainsi, "ma" LIST n'est pas une construction en boucle, mais une construction en boucle/copie. C'est contre-intuitif, mais j'aurais dû m'en souvenir lorsque j'ai commenté une récente question de stackoverflow sur ce point précis: la construction/copie peut être plus efficace pour les objets comptés, etc.
Alors:
vector<T> x(n);
ou
vector<T> x;
x.resize(n);
est - sur de nombreuses implémentations STL - quelque chose comme:
T temp;
for (int i = 0; i < n; ++i)
x[i] = temp;
Le problème étant que la génération actuelle d'optimiseurs de compilateur ne semble pas fonctionner du fait que temp est une gâchis non initialisée et n'optimise pas les invocations du constructeur de la boucle et du constructeur de copie par défaut. Vous pouvez raisonnablement affirmer que les compilateurs ne doivent absolument pas optimiser cela, car un programmeur écrivant ce qui précède s'attend raisonnablement à ce que tous les objets soient identiques après la boucle, même s'il est inutile (mises en garde habituelles à propos de 'identique'/operator == vs memcmp/operator = etc s'appliquent). On ne peut s’attendre à ce que le compilateur ait un aperçu supplémentaire du contexte plus large de std :: vector <> ou de l’utilisation ultérieure des données suggérant que cette optimisation est sans danger.
Cela peut être comparé à la mise en œuvre directe, plus évidente:
for (int i = 0; i < n; ++i)
x[i] = T();
Ce que nous pouvons attendre d’un compilateur pour l’optimiser.
Pour être un peu plus explicite sur la justification de cet aspect du comportement de vector, considérons:
std::vector<big_reference_counted_object> x(10000);
Il est clair que la différence entre 10000 objets indépendants et 10000 référençant les mêmes données constitue une différence majeure. Il existe un argument raisonnable selon lequel l'avantage de la protection des utilisateurs occasionnels du C++ contre une action aussi coûteuse surpasse le très faible coût réel de la construction de copies difficile à optimiser.
RÉPONSE ORIGINALE (pour référence/explication des commentaires): Aucune chance. Le vecteur est aussi rapide qu'un tableau, du moins si vous réservez de l'espace de manière judicieuse. ...
Réponse de Martin York Cela me dérange parce que cela semble être une tentative de dissimuler le problème d'initialisation. Mais il a raison d'identifier la construction par défaut redondante comme source de problèmes de performances.
[EDIT: la réponse de Martin ne suggère plus de changer le constructeur par défaut.]
Pour le problème immédiat, vous pouvez certainement appeler la version à 2 paramètres du ctor vector<Pixel>
:
std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
Cela fonctionne si vous voulez initialiser avec une valeur constante, ce qui est un cas courant. Mais le problème plus général est: Comment initialiser efficacement avec quelque chose de plus compliqué qu'une valeur constante?
Pour cela, vous pouvez utiliser un back_insert_iterator
, Qui est un adaptateur d'itérateur. Voici un exemple avec un vecteur de int
s, bien que l'idée générale fonctionne aussi bien pour Pixel
s:
#include <iterator>
// Simple functor return a list of squares: 1, 4, 9, 16...
struct squares {
squares() { i = 0; }
int operator()() const { ++i; return i * i; }
private:
int i;
};
...
std::vector<int> v;
v.reserve(someSize); // To make insertions efficient
std::generate_n(std::back_inserter(v), someSize, squares());
Vous pouvez également utiliser copy()
ou transform()
au lieu de generate_n()
.
L'inconvénient est que la logique pour construire les valeurs initiales doit être déplacée dans une classe séparée, ce qui est moins pratique que de l'avoir en place (bien que les lambdas en C++ 1x rendent cela beaucoup plus agréable). De plus, je m'attends à ce que cela ne soit toujours pas aussi rapide qu'une version non-STL basée sur malloc()
, mais je m'attends à ce qu'elle soit proche, puisqu'elle ne fait qu'une construction pour chaque élément.
Les vectoriels appellent en plus les constructeurs Pixel.
Chacun provoque près d'un million de courses que vous chronométrez.
edit: alors il y a la boucle externe 1 ... 1000, alors faites-en un milliard d'appels!
edit 2: il serait intéressant de voir le désassemblage pour le cas UseArray. Un optimiseur pourrait optimiser le tout, puisqu'il n'a d'autre effet que de brûler le processeur.
Mon ordinateur portable est Lenova G770 (4 Go de RAM).
Le système d'exploitation est Windows 7 64 bits (celui avec ordinateur portable)
Le compilateur est MinGW 4.6.1.
Le IDE est Code :: Blocks .
Je teste les codes sources du premier post.
Optimisation de l'O2
UseArray terminé en 2.841 secondes
UseVector terminé en 2.548 secondes
UseVectorPushBack terminé en 11.95 secondes
Le tout terminé en 17.342 secondes
pause du système
Optimisation de l'O3
UseArray terminé en 1.452 secondes
UseVector terminé en 2.514 secondes
UseVectorPushBack terminé en 12,967 secondes
Le tout terminé en 16.937 secondes
Il semble que les performances du vecteur soient moins bonnes avec l'optimisation de l'O3.
Si vous changez la boucle en
pixels[i].r = i;
pixels[i].g = i;
pixels[i].b = i;
La vitesse du tableau et du vecteur sous O2 et O3 est presque la même.
Certaines données du profileur (le pixel est aligné sur 32 bits):
g++ -msse3 -O3 -ftree-vectorize -g test.cpp -DNDEBUG && ./a.out
UseVector completed in 3.123 seconds
UseArray completed in 1.847 seconds
UseVectorPushBack completed in 9.186 seconds
The whole thing completed in 14.159 seconds
Blabla
andrey@nv:~$ opannotate --source libcchem/src/a.out | grep "Total samples for file" -A3
Overflow stats not available
* Total samples for file : "/usr/include/c++/4.4/ext/new_allocator.h"
*
* 141008 52.5367
*/
--
* Total samples for file : "/home/andrey/libcchem/src/test.cpp"
*
* 61556 22.9345
*/
--
* Total samples for file : "/usr/include/c++/4.4/bits/stl_vector.h"
*
* 41956 15.6320
*/
--
* Total samples for file : "/usr/include/c++/4.4/bits/stl_uninitialized.h"
*
* 20956 7.8078
*/
--
* Total samples for file : "/usr/include/c++/4.4/bits/stl_construct.h"
*
* 2923 1.0891
*/
Dans allocator
:
: // _GLIBCXX_RESOLVE_LIB_DEFECTS
: // 402. wrong new expression in [some_] allocator::construct
: void
: construct(pointer __p, const _Tp& __val)
141008 52.5367 : { ::new((void *)__p) _Tp(__val); }
vector
:
:void UseVector()
:{ /* UseVector() total: 60121 22.3999 */
...
:
:
10790 4.0201 : for (int i = 0; i < dimension * dimension; ++i) {
:
495 0.1844 : pixels[i].r = 255;
:
12618 4.7012 : pixels[i].g = 0;
:
2253 0.8394 : pixels[i].b = 0;
:
: }
tableau
:void UseArray()
:{ /* UseArray() total: 35191 13.1114 */
:
...
:
136 0.0507 : for (int i = 0; i < dimension * dimension; ++i) {
:
9897 3.6874 : pixels[i].r = 255;
:
3511 1.3081 : pixels[i].g = 0;
:
21647 8.0652 : pixels[i].b = 0;
La majeure partie de la surcharge est dans le constructeur de copie. Par exemple,
std::vector < Pixel > pixels;//(dimension * dimension, Pixel());
pixels.reserve(dimension * dimension);
for (int i = 0; i < dimension * dimension; ++i) {
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
Il a les mêmes performances qu'un tableau.
J'ai fait des tests approfondis que je voulais faire depuis un moment. Autant partager cela.
Ceci est ma machine à double démarrage i7-3770, 16 Go de Ram, x86_64, sur Windows 8.1 et Ubuntu 16.04. Plus d'informations et conclusions, remarques ci-dessous. Testé à la fois sur MSVS 2017 et g ++ (sous Windows et sous Linux).
#include <iostream>
#include <chrono>
//#include <algorithm>
#include <array>
#include <locale>
#include <vector>
#include <queue>
#include <deque>
// Note: total size of array must not exceed 0x7fffffff B = 2,147,483,647B
// which means that largest int array size is 536,870,911
// Also image size cannot be larger than 80,000,000B
constexpr int long g_size = 100000;
int g_A[g_size];
int main()
{
std::locale loc("");
std::cout.imbue(loc);
constexpr int long size = 100000; // largest array stack size
// stack allocated c array
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
int A[size];
for (int i = 0; i < size; i++)
A[i] = i;
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "c-style stack array duration=" << duration / 1000.0 << "ms\n";
std::cout << "c-style stack array size=" << sizeof(A) << "B\n\n";
// global stack c array
start = std::chrono::steady_clock::now();
for (int i = 0; i < g_size; i++)
g_A[i] = i;
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "global c-style stack array duration=" << duration / 1000.0 << "ms\n";
std::cout << "global c-style stack array size=" << sizeof(g_A) << "B\n\n";
// raw c array heap array
start = std::chrono::steady_clock::now();
int* AA = new int[size]; // bad_alloc() if it goes higher than 1,000,000,000
for (int i = 0; i < size; i++)
AA[i] = i;
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "c-style heap array duration=" << duration / 1000.0 << "ms\n";
std::cout << "c-style heap array size=" << sizeof(AA) << "B\n\n";
delete[] AA;
// std::array<>
start = std::chrono::steady_clock::now();
std::array<int, size> AAA;
for (int i = 0; i < size; i++)
AAA[i] = i;
//std::sort(AAA.begin(), AAA.end());
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "std::array duration=" << duration / 1000.0 << "ms\n";
std::cout << "std::array size=" << sizeof(AAA) << "B\n\n";
// std::vector<>
start = std::chrono::steady_clock::now();
std::vector<int> v;
for (int i = 0; i < size; i++)
v.Push_back(i);
//std::sort(v.begin(), v.end());
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "std::vector duration=" << duration / 1000.0 << "ms\n";
std::cout << "std::vector size=" << v.size() * sizeof(v.back()) << "B\n\n";
// std::deque<>
start = std::chrono::steady_clock::now();
std::deque<int> dq;
for (int i = 0; i < size; i++)
dq.Push_back(i);
//std::sort(dq.begin(), dq.end());
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "std::deque duration=" << duration / 1000.0 << "ms\n";
std::cout << "std::deque size=" << dq.size() * sizeof(dq.back()) << "B\n\n";
// std::queue<>
start = std::chrono::steady_clock::now();
std::queue<int> q;
for (int i = 0; i < size; i++)
q.Push(i);
duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::cout << "std::queue duration=" << duration / 1000.0 << "ms\n";
std::cout << "std::queue size=" << q.size() * sizeof(q.front()) << "B\n\n";
}
//////////////////////////////////////////////////////////////////////////////////////////
// with MSVS 2017:
// >> cl /std:c++14 /Wall -O2 array_bench.cpp
//
// c-style stack array duration=0.15ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.130ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.90ms
// c-style heap array size=4B
//
// std::array duration=0.20ms
// std::array size=400,000B
//
// std::vector duration=0.544ms
// std::vector size=400,000B
//
// std::deque duration=1.375ms
// std::deque size=400,000B
//
// std::queue duration=1.491ms
// std::queue size=400,000B
//
//////////////////////////////////////////////////////////////////////////////////////////
//
// with g++ version:
// - (tdm64-1) 5.1.0 on Windows
// - (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 on Ubuntu 16.04
// >> g++ -std=c++14 -Wall -march=native -O2 array_bench.cpp -o array_bench
//
// c-style stack array duration=0ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.124ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.648ms
// c-style heap array size=8B
//
// std::array duration=1ms
// std::array size=400,000B
//
// std::vector duration=0.402ms
// std::vector size=400,000B
//
// std::deque duration=0.234ms
// std::deque size=400,000B
//
// std::queue duration=0.304ms
// std::queue size=400,000
//
//////////////////////////////////////////////////////////////////////////////////////////
Remarques
std::sort()
aussi (vous pouvez le voir commenté), mais je les ai supprimés plus tard car il n'y avait pas de différences relatives significatives.std::array
Entre les exécutions consécutives, tandis que d'autres, notamment les structures std :: data, variaient énormément en comparaisonstd::array
Et les tableaux de style c plus rapidement sous Windows sans optimisationBien sûr, ceci est un code pour une construction optimisée. Et puisque la question portait sur std::vector
, Alors oui, beaucoup! plus lent que les tableaux simples (optimisé/non optimisé). Mais lorsque vous effectuez un test de performance, vous souhaitez naturellement produire un code optimisé.
Pour moi, la vedette de la série a été std::array
.
Voici comment le Push_back
méthode en travaux vectoriels:
Après avoir appelé Push_back
X articles:
Répéter. Si vous n'êtes pas reserving
espace, sa sera certainement plus lente. Plus que cela, s'il est coûteux de copier l'élément alors 'Push_back' comme ça va vous manger vivant.
En ce qui concerne la chose vector
versus tableau, je vais devoir être d'accord avec les autres personnes. Exécutez la version, activez les optimisations, et ajoutez quelques indicateurs supplémentaires pour que les collaborateurs de Microsoft ne le fassent pas # # $ $ ^.
Une dernière chose, si vous n'avez pas besoin de redimensionner, utilisez Boost.Array.
Un meilleur point de repère (je pense ...), compilateur en raison d'optimisations peut changer de code, car les résultats des vecteurs/tableaux alloués ne sont utilisés nulle part. Résultats:
$ g++ test.cpp -o test -O3 -march=native
$ ./test
UseArray inner completed in 0.652 seconds
UseArray completed in 0.773 seconds
UseVector inner completed in 0.638 seconds
UseVector completed in 0.757 seconds
UseVectorPushBack inner completed in 6.732 seconds
UseVectorPush completed in 6.856 seconds
The whole thing completed in 8.387 seconds
Compilateur:
gcc version 6.2.0 20161019 (Debian 6.2.0-9)
CPU:
model name : Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz
Et le code:
#include <cstdlib>
#include <vector>
#include <iostream>
#include <string>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>
class TestTimer
{
public:
TestTimer(const std::string & name) : name(name),
start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
{
}
~TestTimer()
{
using namespace std;
using namespace boost;
posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
posix_time::time_duration d = now - start;
cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
" seconds" << endl;
}
private:
std::string name;
boost::posix_time::ptime start;
};
struct Pixel
{
Pixel()
{
}
Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
{
}
unsigned char r, g, b;
};
void UseVector(std::vector<std::vector<Pixel> >& results)
{
TestTimer t("UseVector inner");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel>& pixels = results.at(i);
pixels.resize(dimension * dimension);
for(int i = 0; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
}
}
void UseVectorPushBack(std::vector<std::vector<Pixel> >& results)
{
TestTimer t("UseVectorPushBack inner");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel>& pixels = results.at(i);
pixels.reserve(dimension * dimension);
for(int i = 0; i < dimension * dimension; ++i)
pixels.Push_back(Pixel(255, 0, 0));
}
}
void UseArray(Pixel** results)
{
TestTimer t("UseArray inner");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);
results[i] = pixels;
for(int i = 0 ; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
// free(pixels);
}
}
void UseArray()
{
TestTimer t("UseArray");
Pixel** array = (Pixel**)malloc(sizeof(Pixel*)* 1000);
UseArray(array);
for(int i=0;i<1000;++i)
free(array[i]);
free(array);
}
void UseVector()
{
TestTimer t("UseVector");
{
std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
UseVector(vector);
}
}
void UseVectorPushBack()
{
TestTimer t("UseVectorPush");
{
std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
UseVectorPushBack(vector);
}
}
int main()
{
TestTimer t1("The whole thing");
UseArray();
UseVector();
UseVectorPushBack();
return 0;
}
Je dois dire que je ne suis pas un expert en C++. Mais pour ajouter quelques résultats d'expériences:
compiler: gcc-6.2.0/bin/g ++ -O3 -std = c ++ 14 vector.cpp
machine:
Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz
OS:
2.6.32-642.13.1.el6.x86_64
Sortie:
UseArray completed in 0.167821 seconds
UseVector completed in 0.134402 seconds
UseConstructor completed in 0.134806 seconds
UseFillConstructor completed in 1.00279 seconds
UseVectorPushBack completed in 6.6887 seconds
The whole thing completed in 8.12888 seconds
Ici, la seule chose qui me semble étrange est la performance "UseFillConstructor" par rapport à "UseConstructor".
Le code:
void UseConstructor()
{
TestTimer t("UseConstructor");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel> pixels(dimension*dimension);
for(int i = 0; i < dimension * dimension; ++i)
{
pixels[i].r = 255;
pixels[i].g = 0;
pixels[i].b = 0;
}
}
}
void UseFillConstructor()
{
TestTimer t("UseFillConstructor");
for(int i = 0; i < 1000; ++i)
{
int dimension = 999;
std::vector<Pixel> pixels(dimension*dimension, Pixel(255,0,0));
}
}
Ainsi, la "valeur" supplémentaire fournie ralentit considérablement les performances, ce qui, selon moi, est dû à plusieurs appels au constructeur de copie. Mais...
Compiler:
gcc-6.2.0/bin/g++ -std=c++14 -O vector.cpp
Sortie:
UseArray completed in 1.02464 seconds
UseVector completed in 1.31056 seconds
UseConstructor completed in 1.47413 seconds
UseFillConstructor completed in 1.01555 seconds
UseVectorPushBack completed in 6.9597 seconds
The whole thing completed in 11.7851 seconds
Donc, dans ce cas, l’optimisation gcc est très importante mais elle ne peut pas vous aider beaucoup quand une valeur est fournie par défaut. Ceci, est contre mes frais de scolarité en fait. J'espère que cela aidera le nouveau programmeur à choisir le format d'initialisation du vecteur.
Je veux juste mentionner que le vecteur (et smart_ptr) est juste une couche mince ajoutée au-dessus des tableaux bruts (et des pointeurs bruts). Et en réalité, le temps d'accès d'un vecteur en mémoire continue est plus rapide que celui d'un tableau. Le code suivant montre le résultat du vecteur et du tableau d’initialisation et d’accès.
#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>
#include <vector>
#define SIZE 20000
int main() {
srand (time(NULL));
vector<vector<int>> vector2d;
vector2d.reserve(SIZE);
int index(0);
boost::posix_time::ptime start_total = boost::posix_time::microsec_clock::local_time();
// timer start - build + access
for (int i = 0; i < SIZE; i++) {
vector2d.Push_back(vector<int>(SIZE));
}
boost::posix_time::ptime start_access = boost::posix_time::microsec_clock::local_time();
// timer start - access
for (int i = 0; i < SIZE; i++) {
index = Rand()%SIZE;
for (int j = 0; j < SIZE; j++) {
vector2d[index][index]++;
}
}
boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration msdiff = end - start_total;
cout << "Vector total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
msdiff = end - start_acess;
cout << "Vector access time: " << msdiff.total_milliseconds() << "milliseconds.\n";
int index(0);
int** raw2d = nullptr;
raw2d = new int*[SIZE];
start_total = boost::posix_time::microsec_clock::local_time();
// timer start - build + access
for (int i = 0; i < SIZE; i++) {
raw2d[i] = new int[SIZE];
}
start_access = boost::posix_time::microsec_clock::local_time();
// timer start - access
for (int i = 0; i < SIZE; i++) {
index = Rand()%SIZE;
for (int j = 0; j < SIZE; j++) {
raw2d[index][index]++;
}
}
end = boost::posix_time::microsec_clock::local_time();
msdiff = end - start_total;
cout << "Array total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
msdiff = end - start_acess;
cout << "Array access time: " << msdiff.total_milliseconds() << "milliseconds.\n";
for (int i = 0; i < SIZE; i++) {
delete [] raw2d[i];
}
return 0;
}
La sortie est:
Vector total time: 925milliseconds.
Vector access time: 4milliseconds.
Array total time: 30milliseconds.
Array access time: 21milliseconds.
Donc, la vitesse sera presque la même si vous l'utilisez correctement. (comme d'autres l'ont mentionné en utilisant reserve () ou resize ()).
En passant, le ralentissement de votre vision dans les classes utilisant des vecteurs se produit également avec des types standard comme int. Voici un code multithread:
#include <iostream>
#include <cstdio>
#include <map>
#include <string>
#include <typeinfo>
#include <vector>
#include <pthread.h>
#include <sstream>
#include <fstream>
using namespace std;
//pthread_mutex_t map_mutex=PTHREAD_MUTEX_INITIALIZER;
long long num=500000000;
int procs=1;
struct iterate
{
int id;
int num;
void * member;
iterate(int a, int b, void *c) : id(a), num(b), member(c) {}
};
//fill out viterate and piterate
void * viterate(void * input)
{
printf("am in viterate\n");
iterate * info=static_cast<iterate *> (input);
// reproduce member type
vector<int> test= *static_cast<vector<int>*> (info->member);
for (int i=info->id; i<test.size(); i+=info->num)
{
//printf("am in viterate loop\n");
test[i];
}
pthread_exit(NULL);
}
void * piterate(void * input)
{
printf("am in piterate\n");
iterate * info=static_cast<iterate *> (input);;
int * test=static_cast<int *> (info->member);
for (int i=info->id; i<num; i+=info->num) {
//printf("am in piterate loop\n");
test[i];
}
pthread_exit(NULL);
}
int main()
{
cout<<"producing vector of size "<<num<<endl;
vector<int> vtest(num);
cout<<"produced a vector of size "<<vtest.size()<<endl;
pthread_t thread[procs];
iterate** it=new iterate*[procs];
int ans;
void *status;
cout<<"begining to thread through the vector\n";
for (int i=0; i<procs; i++) {
it[i]=new iterate(i, procs, (void *) &vtest);
// ans=pthread_create(&thread[i],NULL,viterate, (void *) it[i]);
}
for (int i=0; i<procs; i++) {
pthread_join(thread[i], &status);
}
cout<<"end of threading through the vector";
//reuse the iterate structures
cout<<"producing a pointer with size "<<num<<endl;
int * pint=new int[num];
cout<<"produced a pointer with size "<<num<<endl;
cout<<"begining to thread through the pointer\n";
for (int i=0; i<procs; i++) {
it[i]->member=&pint;
ans=pthread_create(&thread[i], NULL, piterate, (void*) it[i]);
}
for (int i=0; i<procs; i++) {
pthread_join(thread[i], &status);
}
cout<<"end of threading through the pointer\n";
//delete structure array for iterate
for (int i=0; i<procs; i++) {
delete it[i];
}
delete [] it;
//delete pointer
delete [] pint;
cout<<"end of the program"<<endl;
return 0;
}
Le comportement du code montre que l'instanciation du vecteur est la partie la plus longue du code. Une fois que vous avez traversé ce goulot de bouteille. Le reste du code est extrêmement rapide. Cela est vrai quel que soit le nombre de threads que vous exécutez.
En passant, ignorez le nombre absolument fou d'inclusions. J'utilise ce code pour tester des éléments d'un projet, de sorte que le nombre d'inclusions ne cesse de croître.
Avec les bonnes options, les vecteurs et les tableaux peuvent générer un asm identique . Dans ces cas, ils ont bien sûr la même vitesse, car vous obtenez le même fichier exécutable de toute façon.
Eh bien, parce que vector :: resize () effectue beaucoup plus de traitement que l’allocation de mémoire simple (par malloc).
Essayez de placer un point d'arrêt dans votre constructeur de copie (définissez-le de manière à pouvoir le faire!) Et le temps de traitement supplémentaire sera écoulé.