web-dev-qa-db-fra.com

Vecteur: initialisation ou réserve?

Je connais la taille d'un vecteur, quelle est la meilleure procédure pour l'initialiser ?:

option 1

vector<int> vec(3); //in .h
vec.at(0)=var1;     //in .cpp
vec.at(1)=var2;     //in .cpp
vec.at(2)=var3;     //in .cpp

option 2

vector<int> vec;     //in .h
vec.reserve(3);     //in .cpp
vec.Push_back(var1);     //in .cpp
vec.Push_back(var2);     //in .cpp
vec.Push_back(var3);     //in .cpp

Je suppose que l'option 2 est meilleure que 1. est-ce? autres options?

45
Ale

Les deux variantes ont une sémantique différente, c’est-à-dire que vous comparez des pommes et des oranges.

La première vous donne un vecteur de n valeurs initialisées par défaut, la seconde variante réserve la mémoire, mais ne les initialise pas.

Choisissez ce qui correspond le mieux à vos besoins, c’est-à-dire ce qui est "meilleur" dans une situation donnée.

38
Sebastian Mach

Le "meilleur" moyen serait:

vector<int> vec = {var1, var2, var3};

disponible avec un compilateur compatible C++ 11.

Vous ne savez pas exactement ce que vous voulez dire en faisant les choses dans un en-tête ou des fichiers d'implémentation. Un global mutable est un non-non pour moi. S'il s'agit d'un membre de la classe, il peut être initialisé dans la liste d'initialisation du constructeur.

Sinon, l'option 1 serait généralement utilisée si vous savez combien d'éléments vous allez utiliser et les valeurs par défaut (0 pour int) seraient utiles.
Utiliser at ici signifie que vous ne pouvez pas garantir la validité de l'index. Une situation comme celle-ci s’alarme. Même si vous serez en mesure de détecter les problèmes de manière fiable, il est certainement plus simple d'utiliser Push_back et de ne plus vous soucier d'obtenir des index corrects.

Dans le cas de l'option 2, la différence de performance est généralement nulle, que vous réserviez de la mémoire ou non. Il est donc plus simple de ne pas réserver *. Sauf peut-être si le vecteur contient des types qui sont très coûteux à copier (et ne permettent pas un déplacement rapide en C++ 11), ou la taille du vecteur va être énorme.


* De Stroustrups Style et technique C++ FAQ :

Les gens s'inquiètent parfois du coût de la croissance de std :: vector progressivement. Je m'inquiétais de ça et utilisais reserve () to optimiser la croissance. Après avoir mesuré mon code et avoir à plusieurs reprises difficulté à trouver les avantages de performance de reserve () en réel programmes, j’ai arrêté de l’utiliser sauf là où il faut éviter invalidation itérateur (un cas rare dans mon code). Encore une fois: mesurer avant vous optimisez.

39
UncleBens

Bien que vos exemples soient essentiellement les mêmes, il se peut que lorsque le type utilisé n’est pas une int, le choix vous soit choisi. Si votre type n'a pas de constructeur par défaut, ou s'il vous faudra quand même reconstruire chaque élément ultérieurement, j'utiliserais reserve. Ne tombez pas dans le piège que j'ai fait et utilisez reserve puis le operator[] pour l'initialisation!


Constructeur

std::vector<MyType> myVec(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

Dans ce premier cas, en utilisant le constructeur, size et numberOfElementsToStart seront égaux et capacity leur sera supérieurs ou égaux. 

Pensez à myVec comme un vecteur contenant un certain nombre d’éléments de MyType accessibles et modifiables, Push_back(anotherInstanceOfMyType) l’ajoute à la fin du vecteur.


Réserve

std::vector<MyType> myVec;
myVec.reserve(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

Lorsque vous utilisez la fonction reserve, size sera 0 jusqu'à ce que vous ajoutiez un élément au tableau et capacity sera égal ou supérieur à numberOfElementsToStart.

Imaginez myVec comme un vecteur vide auquel de nouveaux éléments peuvent être ajoutés en utilisant Push_backsans allocation de mémoire pour au moins les premiers éléments numberOfElementsToStart.

Notez que Push_back() nécessite toujours une vérification interne pour vous assurer que size <capacité et incrémenter la taille, de sorte que vous souhaiterez peut-être comparer ce coût au coût de la construction par défaut.


Initialisation de la liste

std::vector<MyType> myVec{ var1, var2, var3 };

Il s'agit d'une option supplémentaire pour initialiser votre vecteur. Bien que cela ne soit possible que pour de très petits vecteurs, il s'agit d'un moyen clair d'initialiser un petit vecteur avec des valeurs connues. size sera égal au nombre d'éléments avec lequel vous l'avez initialisé, et capacity sera égal ou supérieur à size. Les compilateurs modernes peuvent optimiser la création d’objets temporaires et éviter les copies inutiles.

7
Troyseph

L'option 2 est préférable, car reserve n'a besoin que de réserver de la mémoire (3 * sizeof (T)), tandis que la première option appelle le constructeur du type de base pour chaque cellule du conteneur.

Pour les types de type C, ce sera probablement le même.

6
nob

D'une manière ou d'une autre, une réponse sans réponse qui est complètement fausse est restée acceptée et la plupart des voix ont été votées pendant environ 7 ans. Ce n'est pas une question de pommes et d'oranges. Ce n'est pas une question à laquelle il faut répondre par de vagues clichés.

Pour une règle simple à suivre:

L'option n ° 1 est plus rapide ... enter image description here  enter image description here

... mais cela ne devrait probablement pas être votre plus grande préoccupation.

Tout d'abord, la différence est assez mineure. Deuxièmement, à mesure que nous optimisons l’optimisation du compilateur, la différence devient encore plus petite. Par exemple, sur mon gcc-5.4.0, la différence est sans doute triviale lors de l'exécution de l'optimisation du compilateur de niveau 3 (-O3):  enter image description here

Donc, en général, je recommanderais d'utiliser la méthode n ° 1 chaque fois que vous rencontrez cette situation. Cependant, si vous ne savez pas lequel est optimal, cela ne vaut probablement pas la peine de le découvrir. Choisissez-en un et continuez, car il est peu probable que cela provoque un ralentissement notable de l'ensemble de votre programme.


Ces tests ont été effectués en échantillonnant des tailles de vecteurs aléatoires à partir d'une distribution normale, puis en programmant l'initialisation des vecteurs de ces tailles à l'aide des deux méthodes. Nous conservons une variable de somme factice pour nous assurer que l'initialisation du vecteur n'est pas optimisée, et nous randomisons les tailles et les valeurs de vecteurs afin d'éviter toute erreur due à la prédiction de branche, à la mise en cache, etc.

main.cpp:

/* 
 * Test constructing and filling a vector in two ways: construction with size
 * then assignment versus construction of empty vector followed by Push_back
 * We collect dummy sums to prevent the compiler from optimizing out computation
 */

#include <iostream>
#include <vector>

#include "rng.hpp"
#include "timer.hpp"

const size_t kMinSize = 1000;
const size_t kMaxSize = 100000;
const double kSizeIncrementFactor = 1.2;
const int kNumVecs = 10000;

int main() {
  for (size_t mean_size = kMinSize; mean_size <= kMaxSize;
       mean_size = static_cast<size_t>(mean_size * kSizeIncrementFactor)) {
    // Generate sizes from normal distribution
    std::vector<size_t> sizes_vec;
    NormalIntRng<size_t> sizes_rng(mean_size, mean_size / 10.0); 
    for (int i = 0; i < kNumVecs; ++i) {
      sizes_vec.Push_back(sizes_rng.GenerateValue());
    }
    Timer timer;
    UniformIntRng<int> values_rng(0, 5);
    // Method 1: construct with size, then assign
    timer.Reset();
    int method_1_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec[i] = values_rng.GenerateValue();
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_1_sum += vec[i];
      }
    }
    double method_1_seconds = timer.GetSeconds();
    // Method 2: reserve then Push_back
    timer.Reset();
    int method_2_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec;
      vec.reserve(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec.Push_back(values_rng.GenerateValue());
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_2_sum += vec[i];
      }
    }
    double method_2_seconds = timer.GetSeconds();
    // Report results as mean_size, method_1_seconds, method_2_seconds
    std::cout << mean_size << ", " << method_1_seconds << ", " << method_2_seconds;
    // Do something with the dummy sums that cannot be optimized out
    std::cout << ((method_1_sum > method_2_sum) ? "" : " ") << std::endl;
  }

  return 0;
}

Les fichiers d'en-tête que j'ai utilisés sont situés ici:

2
Apollys

Une autre option consiste à faire confiance à votre compilateur (tm) et à effectuer le Push_backs sans appeler reserve auparavant. Il doit allouer de l'espace lorsque vous commencez à ajouter des éléments. Peut-être le fait-il aussi bien que vous le feriez?

Il est "préférable" d'avoir un code plus simple qui fait le même travail.

1
Bo Persson

Comment ça marche

Ceci est spécifique à la mise en œuvre, mais en général, la structure de données de Vector en interne aura un pointeur sur le bloc de mémoire où les éléments se trouveraient. GCC et VC++ allouent 0 par défaut. Vous pouvez donc penser que le pointeur de la mémoire interne de Vector est nullptr par défaut.

Lorsque vous appelez vector<int> vec(N); comme dans votre option 1, les N objets sont créés à l'aide du constructeur par défaut. Cela s'appelle fill constructeur.

Lorsque vous utilisez le constructeur vec.reserve(N);after par défaut, comme dans l'option 2, le bloc de données contient 3 éléments mais aucun objet n'est créé contrairement à l'option 1.

Pourquoi choisir l'option 1

Si vous connaissez le nombre d'éléments vectoriels que vous souhaitez conserver et que vous pouvez laisser la plupart des éléments à leurs valeurs par défaut, vous souhaiterez peut-être utiliser cette option. 

Pourquoi choisir l'option 2

Cette option est généralement la meilleure des deux car elle n'affecte que les blocs de données pour une utilisation future et ne remplit pas réellement les objets créés à partir du constructeur par défaut.

1
Shital Shah

À long terme, cela dépend de l'utilisation et du nombre d'éléments. 

Exécutez le programme ci-dessous pour comprendre comment le compilateur réserve de l'espace:

vector<int> vec;
for(int i=0; i<50; i++)
{
  cout << "size=" << vec.size()  << "capacity=" << vec.capacity() << endl;
  vec.Push_back(i);
}

size est le nombre d'éléments réels et la capacité est la taille réelle du tableau vers le vecteur d'implémentation . Mais lorsque la taille est 43, la capacité est 63. En fonction du nombre d'éléments, l'un ou l'autre peut être meilleur. Par exemple, augmenter la capacité peut être coûteux.

1
haberdar

Comme il semble que 5 ans se soient écoulés et qu'une réponse fausse soit toujours acceptée, et que la réponse la plus votée soit totalement inutile (manque la forêt pour les arbres), j'ajouterai une vraie réponse.

Méthode n ° 1 : nous passons un paramètre de taille initiale dans le vecteur (appelons-le n. Cela signifie que le vecteur est rempli d'éléments n, qui seront initialisés à leur valeur par défaut. Par exemple, si le vecteur contient ints , il sera rempli de n zéros.

Méthode n ° 2 : nous créons d'abord un vecteur vide. Ensuite, nous réservons un espace pour les éléments n. Dans ce cas, nous ne créons jamais les éléments n et n'effectuons donc aucune initialisation des éléments du vecteur. Puisque nous prévoyons d’écraser immédiatement les valeurs de chaque élément, le manque d’initialisation ne nous fera pas de mal. D'autre part, puisque nous avons globalement fait moins, ce serait la meilleure * option.

* mieux - définition réelle: jamais pire. Il est toujours possible qu'un compilateur intelligent comprenne ce que vous essayez de faire et l'optimise pour vous.


Conclusion : utilise la méthode n ° 2.

0
Apollys