Comparer emplace_back() et push_back()

Ce texte est un premier jet; je ferai un article plus formel sur le sujet dès que j'aurai quelques minutes...

Tel que mentionné en classe, depuis  C++ 11, les conteneurs standards tendent à offrir des fonctions de type emplace (p. ex. : v.emplace_back(args) pour un conteneur v d'éléments de type X) en plus des fonctions de type push (p. ex. : v.push_back(X{args}) pour un conteneur v d'éléments de type X).

Vaut-il la peine de s'en préoccuper? Jugez par vous-mêmes.

Principal.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <chrono>

struct Point {
   double x = {}, y = {}, z = {};
   Point() = default;
   Point(double x, double y, double z) : x{ x }, y{ y }, z{ z } {
   }
};
int main() {
   using namespace std;
   using namespace std::chrono;
   // la destination: un vecteur
   enum { NTESTS = 50 };
   enum { NELEMS = 10'000'000 };
   milliseconds elapsed_push[NTESTS]{};
   vector<Point> v;
   v.reserve(NELEMS);
   for (int i = 0; i < NTESTS; ++i) {
      v.clear();
      auto avant = high_resolution_clock::now();
      for (int j = 0; j < NELEMS; ++j)
         v.push_back(Point{ 1.0, -1.0, 1.0 });
      auto apres = high_resolution_clock::now();
      elapsed_push[i] = duration_cast<milliseconds>(apres - avant);
   }
   milliseconds elapsed_emplace[NTESTS]{};
   for (int i = 0; i < NTESTS; ++i) {
      v.clear();
      auto avant = high_resolution_clock::now();
      for (int j = 0; j < NELEMS; ++j) {
         v.emplace_back(1.0, -1.0, 1.0);
      }
      auto apres = high_resolution_clock::now();
      elapsed_emplace[i] = duration_cast<milliseconds>(apres - avant);
   }
   sort(begin(elapsed_push), end(elapsed_push));
   auto moyenne_push = accumulate(begin(elapsed_push), end(elapsed_push), milliseconds{}) / static_cast<double>(NTESTS);
   auto moyenne_emplace = accumulate(begin(elapsed_emplace), end(elapsed_emplace), milliseconds{}) / static_cast<double>(NTESTS);
   cout << "Avec " << NTESTS << " tests de push_back() sur " << NELEMS << " elements:\n"
        << "\tmeilleur temps: " << elapsed_push[0].count() << " ms.\n"
        << "\tpire temps: " << elapsed_push[NTESTS - 1].count() << " ms.\n"
        << "\ttemps moyen: " << moyenne_push.count() << " ms.\n" << endl;
   sort(begin(elapsed_emplace), end(elapsed_emplace));
   cout << "Avec " << NTESTS << " tests de emplace() sur " << NELEMS << " elements:\n"
        << "\tmeilleur temps: " << elapsed_emplace[0].count() << " ms.\n"
        << "\tpire temps: " << elapsed_emplace[NTESTS - 1].count() << " ms.\n"
        << "\ttemps moyen: " << moyenne_emplace.count() << " ms.\n" << endl;
   cout << "Pour esssentiellement le meme travail,\n"
        << "emplace_back() requiert en moyenne "
        << moyenne_emplace / moyenne_push * 100.0
        << "% du temps requis par push_back()\n" << endl;
}

À l'exécution (en mode Release, bien entendu), sur mon poste de travail, cela donne :

Visual Studio 2017 gcc à travers ideone.com (lien)
Avec 50 tests de push_back() sur 10000000 elements:
        meilleur temps: 51 ms.
        pire temps: 110 ms.
        temps moyen: 52.26 ms.

Avec 50 tests de emplace() sur 10000000 elements:
        meilleur temps: 61 ms.
        pire temps: 62 ms.
        temps moyen: 61.08 ms.

Pour esssentiellement le meme travail,
emplace_back() requiert en moyenne 116.877% du temps requis par push_back()
Avec 50 tests de push_back() sur 10000000 elements:
	meilleur temps: 34 ms.
	pire temps: 63 ms.
	temps moyen: 36.76 ms.

Avec 50 tests de emplace() sur 10000000 elements:
	meilleur temps: 33 ms.
	pire temps: 40 ms.
	temps moyen: 34.94 ms.

Pour esssentiellement le meme travail,
emplace_back() requiert en moyenne 95.049% du temps requis par push_back()

Pas mal, n'est-ce pas? On remarquera qu'avec Visual Studio 2017, chose amusante, push_back() offre un meilleur « meilleur temps » et un meilleur temps moyen, mais un pire cas nettement plus douloureux, ce qui le rend moins pertinent pour un système temps réel. Pour gcc, emplace_back() gagne sur tous les fronts. Maintenant, si nous rendons les valeurs passées au constructeur de Point moins susceptibles de se voir optimisées par voie de constexpr, en faisant un léger ajustement au code source :

#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <chrono>

struct Point {
   double x = {}, y = {}, z = {};
   Point() = default;
   Point(double x, double y, double z) : x{ x }, y{ y }, z{ z } {
   }
};
int main() {
   using namespace std;
   using namespace std::chrono;
   // la destination: un vecteur
   enum { NTESTS = 50 };
   enum { NELEMS = 10'000'000 };
   milliseconds elapsed_push[NTESTS]{};
   vector<Point> v;
   v.reserve(NELEMS);
   for (int i = 0; i < NTESTS; ++i) {
      v.clear();
      auto avant = high_resolution_clock::now();
      for (int j = 0; j < NELEMS; ++j)
         v.push_back(Point{ j / 1.0, j / -1.0, j / 1.0 });
      auto apres = high_resolution_clock::now();
      elapsed_push[i] = duration_cast<milliseconds>(apres - avant);
   }
   milliseconds elapsed_emplace[NTESTS]{};
   for (int i = 0; i < NTESTS; ++i) {
      v.clear();
      auto avant = high_resolution_clock::now();
      for (int j = 0; j < NELEMS; ++j) {
         v.emplace_back(j / 1.0, j / -1.0, j / 1.0);
      }
      auto apres = high_resolution_clock::now();
      elapsed_emplace[i] = duration_cast<milliseconds>(apres - avant);
   }
   sort(begin(elapsed_push), end(elapsed_push));
   auto moyenne_push = accumulate(begin(elapsed_push), end(elapsed_push), milliseconds{}) / static_cast<double>(NTESTS);
   auto moyenne_emplace = accumulate(begin(elapsed_emplace), end(elapsed_emplace), milliseconds{}) / static_cast<double>(NTESTS);
   cout << "Avec " << NTESTS << " tests de push_back() sur " << NELEMS << " elements:\n"
        << "\tmeilleur temps: " << elapsed_push[0].count() << " ms.\n"
        << "\tpire temps: " << elapsed_push[NTESTS - 1].count() << " ms.\n"
        << "\ttemps moyen: " << moyenne_push.count() << " ms.\n" << endl;
   sort(begin(elapsed_emplace), end(elapsed_emplace));
   cout << "Avec " << NTESTS << " tests de emplace() sur " << NELEMS << " elements:\n"
        << "\tmeilleur temps: " << elapsed_emplace[0].count() << " ms.\n"
        << "\tpire temps: " << elapsed_emplace[NTESTS - 1].count() << " ms.\n"
        << "\ttemps moyen: " << moyenne_emplace.count() << " ms.\n" << endl;
   cout << "Pour esssentiellement le meme travail,\n"
        << "emplace_back() requiert en moyenne "
        << moyenne_emplace / moyenne_push * 100.0
        << "% du temps requis par push_back()\n" << endl;
}

... alors la situation met en relief la force relative d'emplace_back() :

Visual Studio 2017 gcc à travers ideone.com (lien)
Avec 50 tests de push_back() sur 10000000 elements:
        meilleur temps: 90 ms.
        pire temps: 131 ms.
        temps moyen: 91.14 ms.

Avec 50 tests de emplace() sur 10000000 elements:
        meilleur temps: 60 ms.
        pire temps: 62 ms.
        temps moyen: 61.06 ms.

Pour esssentiellement le meme travail,
emplace_back() requiert en moyenne 66.9958% du temps requis par push_back()
Avec 50 tests de push_back() sur 10000000 elements:
	meilleur temps: 34 ms.
	pire temps: 71 ms.
	temps moyen: 36.6 ms.

Avec 50 tests de emplace() sur 10000000 elements:
	meilleur temps: 34 ms.
	pire temps: 43 ms.
	temps moyen: 37.56 ms.

Pour esssentiellement le meme travail,
emplace_back() requiert en moyenne 102.623% du temps requis par push_back()

... où la situation demeure la même, mais inversée du point de vue des compilateurs : avec Visual Studio 2017, emplace_back() gagne sur tous les fronts, et de beaucoup, alors qu'avec gcc, push_back() semble très légèrement plus rapide, mais beaucoup moins stable.


Valid XHTML 1.0 Transitional

CSS Valide !