Existe-t-il une différence de performances entre les deux méthodes ci-dessous pour insérer de nouveaux éléments à la fin d'un std::vector
:
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::begin(arr), std::end(arr));
Personnellement, j'aime méthode 2 car il est agréable et concis et insère tous les nouveaux éléments d'un tableau en une seule fois. Mais
La raison pour laquelle je n'initialise pas le vecteur avec tous les éléments, pour commencer, c'est que dans mon programme j'ajoute les éléments restants en fonction d'une condition.
Après tout, ils font la même chose. Non?
Non, ils sont différents. La première méthode utilisant std::vector::Push_back
subira plusieurs réallocations par rapport à std::vector::insert
.
Le insert
allouera de la mémoire en interne, selon le courant std::vector::capacity
avant de copier la plage. Voir la discussion suivante pour en savoir plus:
Est-ce que std :: vector :: insert reserve par définition?
Mais y a-t-il une différence de performances?
Pour la raison expliquée ci-dessus, la deuxième méthode montrerait une légère amélioration des performances. Par exemple, voir la marque benck rapide ci-dessous, en utilisant http://quick-bench.com:
Ou écrivez un programme de test pour mesurer les performances (comme @ Quelqu'un de programmeur mentionné dans les commentaires). Voici un exemple de programme de test:
#include <iostream>
#include <chrono>
#include <algorithm>
#include <vector>
using namespace std::chrono;
class Timer final
{
private:
time_point<high_resolution_clock> _startTime;
public:
Timer() noexcept
: _startTime{ high_resolution_clock::now() }
{}
~Timer() noexcept { Stop(); }
void Stop() noexcept
{
const auto endTime = high_resolution_clock::now();
const auto start = time_point_cast<microseconds>(_startTime).time_since_Epoch();
const auto end = time_point_cast<microseconds>(endTime).time_since_Epoch();
const auto durationTaken = end - start;
const auto duration_ms = durationTaken * 0.001;
std::cout << durationTaken.count() << "us (" << duration_ms.count() << "ms)\n";
}
};
// Method 1: Push_back
void Push_back()
{
std::cout << "Push_backing: ";
Timer time{};
for (auto i{ 0ULL }; i < 1000'000; ++i)
{
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
}
}
// Method 2: insert_range
void insert_range()
{
std::cout << "range-inserting: ";
Timer time{};
for (auto i{ 0ULL }; i < 1000'000; ++i)
{
std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::cbegin(arr), std::cend(arr));
}
}
int main()
{
Push_back();
insert_range();
return 0;
}
création de version avec mon système (MSVS2019: /Ox/std: c ++ 17, AMD Ryzen 7 2700x(8 cœurs, 3,70 GHz), x64 Windows 10)
// Build - 1
Push_backing: 285199us (285.199ms)
range-inserting: 103388us (103.388ms)
// Build - 2
Push_backing: 280378us (280.378ms)
range-inserting: 104032us (104.032ms)
// Build - 3
Push_backing: 281818us (281.818ms)
range-inserting: 102803us (102.803ms)
Qui montre pour le scénario donné, std::vector::insert
ing concerne 2.7
fois plus rapide que std::vector::Push_back
.
Voir ce que les autres compilateurs ( clang 8.0 et gcc 9.2) veulent dire, selon leurs implémentations: https://godbolt.org/z/DQrq51
Il peut y avoir une différence entre les deux approches si le vecteur doit se réallouer.
Votre deuxième méthode, appelant une fois la fonction membre insert()
avec une plage d'itérateurs:
vec.insert(std::end(vec), std::begin(arr), std::end(arr));
serait en mesure de fournir l'optimisation de l'allocation de toute la mémoire nécessaire à l'insertion des éléments en un seul coup puisque insert()
obtient itérateurs d'accès aléatoire, c'est-à-dire qu'il faut un temps constant pour connaître la taille de la plage, donc toute l'allocation de mémoire peut être effectuée avant de copier les éléments, et aucune réallocation pendant l'appel ne suivrait.
Votre première méthode, les appels individuels à la fonction membre Push_back()
, peut déclencher plusieurs réallocations, selon le nombre d'éléments à insérer et la mémoire initialement réservée au vecteur.
Notez que l'optimisation expliquée ci-dessus peut ne pas être disponible pour forward ou itérateurs bidirectionnels car il faudrait un temps linéaire dans la taille de la plage pour connaître le nombre d'éléments à insérer . Cependant, le temps nécessaire pour plusieurs allocations de mémoire éclipse probablement le temps nécessaire pour calculer la longueur de la plage pour ces cas, donc ils implémentent probablement toujours cette optimisation. Pour itérateurs d'entrée, cette optimisation n'est même pas possible car ce sont des itérateurs à un seul passage.
Le principal facteur contributif sera les réaffectations. vector
doit faire de la place pour de nouveaux éléments.
Considérez ces 3 sinppets.
//pushback
std::vector<int> vec = {1};
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
//insert
std::vector<int> vec = {1};
int arr[] = {2,3,4,5};
vec.insert(std::end(vec), std::begin(arr), std::end(arr));
//cosntruct
std::vector<int> vec = {1,2,3,4,5};
Pour confirmer les réallocations entrant dans l'image, après avoir ajouté une vec.reserve(5)
dans les versions pushback et insert, nous obtenons les résultats ci-dessous.
Push_back
insère un seul élément, donc dans le pire des cas, vous pouvez rencontrer plusieurs réallocations.
Pour les besoins de l'exemple, considérons le cas où la capacité initiale est de 2 et augmente d'un facteur 2 à chaque réallocation. ensuite
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3); // need to reallocate, capacity is 4
vec.Push_back(4);
vec.Push_back(5); // need to reallocate, capacity is 8
Vous pouvez bien sûr éviter les réaffectations inutiles en appelant
vec.reserve(num_elements_to_Push);
Cependant, si vous insérez de toute façon à partir d'un tableau, la façon la plus idomatique est d'utiliser insert
.