Passer un paramètre par valeur ou par référence-vers-const?

Ce qui suit a été écrit dans une séance en classe suite à une pertinente question des chics Luc Lessard et Pierre-Marc Bérubé, étudiants de la cohorte 07 du DDJV à l'Université de Sherbrooke.

Notez que les équations dans ce document sont affichées à l'aide de MathJax. Notez aussi que j'ai utilisé std::rand(), un générateur de nombres pseudoaléatoires aujourd'hui déprécié, dans le but de simplifier le propos, mais que vous devriez éviter de faire de même dans du code de production; voir ../Divers--cplusplus/prng.html pour en savoir plus.

Pour comprendre la fonction test que nous utiliserons, voir ../AuSecours/Mesurer-le-temps.html

Quand doit-on passer un paramètre par valeur ou par référence-vers-const? En C++, langage où les objets sont souvent manipulés directement plutôt qu'indirectement, comme en Java ou en C#, cette question a un impact sur la vitesse d'exécution des programmes. Ce qui suit escamote évidemment le cas des références non-const, car ce cas n'est pas sémantiquement équivalent à un passage par valeur (la fonction recevant la référence en paramètre peut accéder au référé de manière à le modifier, du moins si l'interface du référé le permet).

Supposons le code à droite, où une instance de la classe X est passée par valeur (fonction f()), ou par référence-vers-const (fonctions g() et h()). Quels sont les enjeux?

  • Si copier un X est coûteux, donc si le constructeur de copie de X ou sont destructeur sont lourds (allocation de ressources et tout le tralala), il est probable que g() soit plus rapide que f()
  • Cependant, si la copie d'un X est peu dispendieuse, il est possible que f() soit plus rapide que g(), du fait que dans f(), les accès à x sont directs, contrairement à g() qui manipule une référence, donc une indirection, et dont tous les accès à x se font en deux temps
  • Dans le doute, mieux vaut faire des tests, surtout s'il s'avère que les fonctions à comparer sont utilisées dans un lieu critique du programme et sont susceptibles d'avoir une incidence sur sa performance d'ensemble

Une chose est hautement probable, cependant : si vous écrivez du code comme celui de h(), vous voulez probablement le remplacer par celui de f() qui fait la même chose, mais mieux.

class X {
   // ...
};
void f(X x) {
   // utiliser x
}
void g(const X &x) {
   // utiliser x
}
void h(const X &x) {
   X temp = x;
   // utiliser temp
}

Plusieurs autres enjeux sont à considérer dans le choix d'un type de passage de paramètre ou de l'autre, en particulier le contexte. Si vous avez recours à de la multiprogrammation, et s'il y a un risque que l'objet original soit manipulé en écriture par un autre thread, alors mieux vaut avoir recours à une copie. La qualification const n'est applicable que localement, après tout, et rien n'est plus surprenant que de manipuler une « constante » qui, par l'action d'un autre tronçon de programme, change subitement d'état.

Dans ce petit article, nous nous intéresserons surtout à la question de la taille des objets. À partir de quel moment est-il préférable d'éviter une copie, même pour un POD dont la Sainte-Trinité est implictement correcte? Cette question touche directement une partie de mes étudiants, qui manipulent une grande quantité de D3DXVECTOR3, vecteurs 3D de DirectX, que je remplacerai ci-dessous par un simulacre nommé TiVecteur par souci de simplicité.

Ces objets (ceux de DirectX et le TiVecteur que j'utiliserai) sont des agrégats simples de trois nombres à virgule flottante, donc des POD, auxquels sont ajoutés des constructeurs par souci de simplicité.

Pour notre première version, nous utiliserons un TiVecteur à trois dimensions, tel que :

sizeof(TiVecteur) >= 3 * sizeof(float)

Nous utiliserons des variantes de plus petite taille plus bas, pour fins de comparaison.

struct TiVecteur {
   float x{}, y{}, z{};
   TiVecteur() = default;
   TiVecteur(float x, float y, float z) noexcept
      : x{ x }, y{ y }, z{ z }
   {
   }
};

Notez au passage la syntaxe que j'ai choisi d'utiliser ici pour les attributs (publics, noms destinés à être utilisés par le code client) et pour les noms des paramètres (le même que celui des attributs, mais en préconstruction ceci ne provoque pas d'ambiguïté) passés au constructeur paramétrique. Sans que je ne sois un fervent défenseur de cette approche, elle est légale et, occasionnellement, préconisée par des penseurs tels qu'Andrew Koenig, dans un cas comme celui-ci où l'objet, de par ses états, est sa propre interface.

Examinons maintenant le code de test auquel nous aurons recours pour réaliser le comparatif en question. Notre première version montrera le code de test en entier, alors que les versions subséquentes se limiteront à présenter les nuances et versions alternatives.

Tout d'abord, pour faciliter la rédaction d'un code de génération d'instances de TiVecteur, j'utiliserai une fonction génératrice (une méthode de classe dans TiVecteur aurait aussi été convenable ici).

Pour les besoins de la cause, chaque TiVecteur aura des coordonnées situées inclusivement entre 0.0f et 1.0f. Je créerai chaque coordonnée avec une expression λ pour garder le tout concis.

#include <cstdlib>
TiVecteur creer() {
   using std::rand;
   auto creer_elem = [] {
      return static_cast<float>(rand())/RAND_MAX;
   };
   return { creer_elem(), creer_elem(), creer_elem() };
}

La fonction par_copie() réalisera une opération sur une copie d'un TiVecteur, alors que la fonction par_ref_to_const() réalisera la même opération sur une référence vers un TiVecteur qualifié const.

L'idée ici est de répéter plusieurs fois ces opérations pour constater leur poids en termes de temps d'exécution.

float par_copie(TiVecteur tv) {
   return tv.x + tv.y + tv.z;
}
float par_ref_to_const(const TiVecteur &tv) {
   return tv.x + tv.y + tv.z;
}

Le programme de test sera simple :

  • Tout d'abord, un vecteur standard sera initialisé de manière à contenir une grande quantité d'instance de TiVecteur, tous créés « au hasard » (ici, le générateur de nombres pseudo-aléatoires n'est pas initialisé pour faire en sorte que les tests soient stables)
  • Notez brièvement que l'opération la plus lente ici est probablement d'allouer dynamiquement l'espace pour N instances de TiVecteur, étant donné la valeur de N
  • Notez aussi qu'un test comme celui-ci n'a de sens que sur du code compilé avec pleine optimisation. Sur un code « en mode Debug », les résultats ne sont pas pertinents
#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>
#include <iterator>
#include <chrono>
#include <utility>
using namespace std;
using namespace std::chrono;
template <class F, class ... Args>
   auto test(F f, Args &&... args) {
      auto pre = high_resolution_clock::now();
      auto res = f(std::forward<Args>(args)...);
      auto post = high_resolution_clock::now();
      return pair{ res, post - pre };
   }
int main() {
   enum { N = 100'000'000 };
   vector<TiVecteur> v;
   v.reserve(N);
   generate_n(back_inserter(v), int{N}, creer);
  • Ensuite, pour les fins du test, nous appelons plusieurs fois chacune de nos deux fonctions de test (plus haut), et nous cumulons une somme bidon que nous afficherons pour empêcher le compilateur d'optimiser le code à un point tel que ces fonctions, somme toute inutiles, ne soient pas appelées
   auto [r0, dt0] = test([&v] {
      return accumulate(
         begin(v), end(v), float{}, [](float a_date, const TiVecteur &tv) {
            return a_date + par_ref_to_const(tv);
         });
   });
   cout << "Temps total: "
        << duration_cast<milliseconds>(dt0).count()
        << " ms." << endl;
   cout << r0 << endl;
   auto [r1, dt1] = test([&v] {
      return accumulate(
         begin(v), end(v), 0.0f, [](float a_date, TiVecteur tv) {
            return a_date + par_copie(tv);
         });
   });
   cout << "Temps total: "
        << duration_cast<milliseconds>(dt1).count()
        << " ms." << endl;
   cout << r1 << endl;
}

Les résultats à l'exécution de ces tests, sur mon ordinateur portatif, vont comme suit (l'affichage de la somme cumulée est purement bidon, évidemment) :

Temps total: 560 ms.
6.71089e+007
Temps total: 718 ms.
6.71089e+007
Appuyez sur une touche pour continuer...

Ainsi, malgré les accès indirects aux attributs, la version référence-vers-const est ici significativement plus rapide que la version recevant un paramètre par valeur. Notez que nous utilisons ici des objets plus gros que la taille des registres, du moins sur une machine 32 bits.

Pour voir l'impact d'une réduction de la taille des objets sur les temps de passage de paramètres, réduisons notre classe TiVecteur de manière à en faire un vecteur 2D.

struct TiVecteur {
   float x{}, y{};
   TiVecteur() = default;
   TiVecteur(float x, float y) : x{ x }, y{ y } {
   }
};

Adaptons bien sûr nos outils auxiliaires pour tenir compte de ce changement structurel.

Pour le reste, notre programme principal ne changera en rien, étant complètement découplé de la structure interne de notre type de données.

TiVecteur creer() {
   auto creer_elem = [] {
      return static_cast<float>(rand())/RAND_MAX;
   };
   return { creer_elem(), creer_elem() };
}
float par_copie(TiVecteur tv) {
   return tv.x + tv.y;
}
float par_ref_to_const(const TiVecteur &tv) {
   return tv.x + tv.y;
}

Les résultats à l'exécution de ces tests, sur mon ordinateur portatif, vont comme suit :

Temps total: 515 ms.
3.35544e+007
Temps total: 697 ms.
3.35544e+007
Appuyez sur une touche pour continuer...

Encore une fois, la version appliquant la référence-vers-const est nettement plus rapide que celle procédant par valeur.

Enfin, si nous utilisons une copie locale du TiVecteur original dans la version procédant par référence-vers-const, nous obtenons le code à droite.

float par_copie(TiVecteur tv) {
   return tv.x += tv.y;
}
float par_ref_to_const(const TiVecteur &tv) {
   TiVecteur t = tv;
   return t.x += t.y;
}

Les résultats à l'exécution de ces tests, sur mon ordinateur portatif, vont alors comme suit :

Temps total: 695 ms.
3.35544e+007
Temps total: 673 ms.
3.35544e+007
Appuyez sur une touche pour continuer...

Sans grande surprise, la copie manuelle est moins efficace (de peu) que celle, implicite, faite par le compilateur.

À titre informatif, si nous réduisons la taille d'un TiVecteur à celle d'un float, bien qu'il n'ait alors plus d'un vecteur que le nom, qu'adviendra-t-il des performances relatives des deux types de passage de paramètres?

Les résultats à l'exécution de ces tests, sur mon ordinateur portatif, vont alors comme suit :

Temps total: 515 ms.
3.35544e+007
Temps total: 629 ms.
3.35544e+007
Appuyez sur une touche pour continuer...

Assez étonamment ici, le type dont le constructeur est trivial (au sens mathématique du terme) et dont la taille est celle d'un registre est plus rapidement manipulé par une fonction lorsque passé par référence-vers-const.

struct TiVecteur {
   float x{};
   TiVecteur() = default;
   TiVecteur(float x) : x{ x } {
   }
};
TiVecteur creer() {
   auto creer_elem = [] {
      return static_cast<float>(rand())/RAND_MAX;
   };
   return { creer_elem() };
}
float par_copie(TiVecteur tv) {
   return tv.x + tv.x;
}
float par_ref_to_const(const TiVecteur &tv) {
   return tv.x + tv.x;
}

Tests un peu plus poussés

Ce qui suit utilise entre autres les listes de types, une technique de métaprogrammation mise de l'avant par Andrei Alexandrescu. Vous voudrez peut-être lire sur le sujet avant de procéder.

Dans le but de mieux comprendre la dernière gamme de résultats, je me suis permis une série supplémentaire de tests. Dans ce qui suit, nous construirons une liste de types, puis nous testerons des appels de fonctions recevant en paramètre des instances de ces types (a) dans un enrobage simple passé par valeur, (b) dans un enrobage simple passé par référence-vers-const, et (c) par valeur, sans enrobage. Nous obtiendrons ainsi des comparatifs de performances dans chaque cas.

Notez que, dans le cas de types dont la copie est nécessairement coûteuse (par exemple des instances de std::string, qui sont responsables de la gestion de mémoire allouée dynamiquement pour les données entreposées à l'interne), nous adaptons les tests de manière à ce que les tests soient résolus dans un temps raisonnable, et ne provoquent pas un épuisement des ressources disponibles (un manque de mémoire).

Les listes de types nous permettront de construire la liste des types pour lesquels nous souhaitons réaliser des tests, et de réduire par le fait-même la redondance de code dans nos sources.

Les macros MAKE_TLIST_n(), pour , sont utilisées pour alléger l'écriture, et ne sont pas nécessaires en tant que telles. Je me suis arrêté à une liste de dix types, mais on aurait pu continuer (tout est question de patience).

Notez que cette partie de l'exemple devrait être modernisée pour utiliser une forme variadique. Je le ferai quand j'aurai quelques minutes.

template <class, class>
   struct type_list;
class Vide {};
#define MAKE_TLIST_0(T) \
   type_list<T,Vide>
#define MAKE_TLIST_1(T0,T1) \
   type_list<T1,MAKE_TLIST_0(T0)>
#define MAKE_TLIST_2(T0,T1,T2) \
   type_list<T2,MAKE_TLIST_1(T0,T1)>
#define MAKE_TLIST_3(T0,T1,T2,T3) \
   type_list<T3,MAKE_TLIST_2(T0,T1,T2)>
#define MAKE_TLIST_4(T0,T1,T2,T3,T4) \
   type_list<T4,MAKE_TLIST_3(T0,T1,T2,T3)>
#define MAKE_TLIST_5(T0,T1,T2,T3,T4,T5) \
   type_list<T5,MAKE_TLIST_4(T0,T1,T2,T3,T4)>
#define MAKE_TLIST_6(T0,T1,T2,T3,T4,T5,T6) \
   type_list<T6,MAKE_TLIST_5(T0,T1,T2,T3,T4,T5)>
#define MAKE_TLIST_7(T0,T1,T2,T3,T4,T5,T6,T7) \
   type_list<T7,MAKE_TLIST_6(T0,T1,T2,T3,T4,T5,T6)>
#define MAKE_TLIST_8(T0,T1,T2,T3,T4,T5,T6,T7,T8) \
   type_list<T8,MAKE_TLIST_7(T0,T1,T2,T3,T4,T5,T6,T7)>
#define MAKE_TLIST_9(T0,T1,T2,T3,T4,T5,T6,T7,T8,T9) \
   type_list<T9,MAKE_TLIST_8(T0,T1,T2,T3,T4,T5,T6,T7,T8)>

Pour les besoins de la cause, nous utiliserons des Wrap<T> pour enrobages autour de types T, et nous les instancierons à partir de fonctions génératrices.

En général, nous créerons des T à partir de valeurs générées pseudo-aléatoirement, mais nous spécialiserons les fonctions génératrices pour les types en fonction desquels cette stratégie un peu simpliste ne convient pas (ici, pour le type std::string, mais nous couvrirons d'autres cas plus bas, pour des points 2D et pour des points 3D).

template <class T>
   struct Wrap {
      T valeur;
      Wrap(T valeur) : valeur{valeur} {
      }
   };
#include <cstdlib>
template <class T>
   Wrap<T> creer_wrap() {
      using std::rand;
      return { static_cast<T>(rand() % 100) };
   }
#include <string>
template <>
   Wrap<std::string> creer_wrap<std::string>() {
      using namespace std;
      return { string(rand() % 10, 'A') };
   }

Les divers types de passages de paramètres testés seront représentés par les fonctions par_valeur(), par_ref_to_const() et pass_thru().

Les deux premières s'appliquent à des Wrap<T> alors que la dernière s'applique à des T bruts. Je n'ai pas couvert le cas const T& brut explicitement, puisqu'il suffit de faire en sortie que le type T passé à pass_thru() soit const & pour le couvrir implicitement.

template <class T>
   T par_valeur(Wrap<T> w) {
      return w.valeur;
   }
template <class T>
   T par_ref_to_const(const Wrap<T> &w) {
      return w.valeur;
   }
template <class T>
   T pass_thru(T valeur) {
      return valeur;
   }

Pour éviter de polluer l'affichage des résultats par des sorties peu pertinentes, nous utiliserons un flux bidon, global, que j'ai nommé ici nullout. Ce sera le pseudo /dev/null de notre programme.

#include <ostream>
#include <ctime>
#include <algorithm>
#include <numeric>
#include <fstream>
#include <chrono>
std::ofstream nullout;

Sur le compilateur utilisé pour mes tests, les noms obtenus par RTTI pour les types primitifs convenaient à mes besoins, mais ceux pour les templates comme std::string, qui est en fait un alias pour le type suivant :

class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>

...ce qui est un peu verbeux. J'ai donc défini des traits pour obtenir des noms plus près de ce que je souhaitais avoir pour fins d'affichage.

template <class T>
   struct traits_types {
      static const char* name() {
         return typeid(T).name();
      }
   };
template <>
   struct traits_types<std::string> {
      static constexpr const char* name() {
         return "string";
      }
   };

Les tests en soi seront plus simples que le code ne le suggère :

  • Nous projetons tout d'abord quelques informations sur les types impliqués et sur le nombre de tests à réaliser (un par élément du vecteur source)
  • Pour chaque type de test, nous réalisons une accumulation des valeurs des divers éléments du vecteur source. Notez que dans certains cas (ceux de types tels que std::string), ceci peut être extrêmement coûteux dû aux multiples allocations de mémoire
  • Nous affichons chaque fois le cumul sur nullout, pour forcer le compilateur à générer le code de test (s'il n'y a aucun effet secondaire à nos calculs, le compilateur les éliminera sans gêne)

Le code à droite peut être simplifié de plusieurs manières, si la chose vous amuse :

  • Ajouter un chronomètre RAII pour saisir les temps avant et après un test
  • Remplacer l'affichage du temps écoulé par une fonction

Évidemment, le choix d'appels à std::accumulate() pour les tests est arbitraire. Vous pouvez procéder autrement si le coeur vous en dit.

 

#include <vector>
#include <iterator>
template <class T>
   void tester(std::ostream &os, const std::vector<Wrap<T>> &v) {
      using namespace std;
      using namespace std::chrono;
      const char *nom = traits_types<T>::name();
      os << "Type " << nom << ", "
         << "sizeof(" << nom << "): " << sizeof(T) << ", "
         << "sizeof(Wrap<" << nom << ">): " << sizeof(Wrap<T>)
         << ", " << v.size() << " tests"
         << endl;
      cout << "Type " << nom << ", "
           << "sizeof(" << nom << "): " << sizeof(T) << ", "
           << "sizeof(Wrap<" << nom << ">): " << sizeof(Wrap<T>)
           << ", " << v.size() << " tests"
           << endl;
      {
         auto avant = system_clock::now();
         T res = accumulate(begin(v), end(v), T(),
                            [](T so_far, Wrap<T> w) {
            return so_far + par_valeur(w);
         });
         auto apres = system_clock::now();
         os << "\tWrap<" << nom << ">, par valeur: "
            << duration_cast<milliseconds>(apres-avant)
            << " ms." << endl;
         nullout << "\tBidon: " << res << endl;
      }
      {
         auto avant = system_clock::now();
         T res = accumulate(begin(v), end(v), T(),
                            [](T so_far, const Wrap<T> &w) {
            return so_far + par_ref_to_const(w);
         });
         auto apres = system_clock::now();
         os << "\tWrap<" << nom << ">, par ref-to-const: "
            << duration_cast<milliseconds>(apres-avant)
            << " ms." << endl;
         nullout << "\tBidon: " << res << endl;
      }
      vector<T> raw;
      transform(begin(v), end(v), back_inserter(raw),
                     [](const Wrap<T> &w) {
         return w.valeur;
      });
      {
         auto avant = system_clock::now();
         T res = accumulate(begin(raw), end(raw), T(),
                            [](T so_far, T val) {
            return so_far + pass_thru(val);
         });
         auto apres = system_clock::now();
         os << "\t" << nom << ", par valeur: "
            << duration_cast<milliseconds>(apres-avant)
            << " ms." << endl;
         nullout << "\tBidon: " << res << endl;
      }
      {
         auto avant = system_clock::now();
         T res = accumulate(begin(raw), end(raw), T(),
                            [](T so_far, const T &val) {
            return so_far + pass_thru(val);
         });
         auto apres = system_clock::now();
         os << "\t" << nom << ", par ref-to-const: "
            << duration_cast<milliseconds>(apres-avant)
            << " ms." << endl;
         nullout << "\tBidon: " << res << endl;
      }
   }

Étant donné la différence importante entre les temps de tests sur des primitifs et sur des objets responsables d'allouer dynamiquement de la mémoire, j'ai choisi d'ajouter une indirection pour déterminer le nombre de tests à faire, pour réduire le nombre de tests dans le cas de ces types. Le facteur de réduction choisi ici est empirique.

template <class, int N>
   struct traits_taille {
      enum { value = N };
   };
template <int N>
   struct traits_taille<std::string, N> {
      enum { value = N / 500 };
   };

La fonction tester() construit un vecteur standard de Wrap<T>, et base la taille de ce vecteur sur les traits de taille associés au type T, le tout avant de débuter les tests à proprement dit.

Les affichages des tests se feront sur std::clog, qui correspondra au fichier choisi en fonction des besoins (ici, dans main()).

#include <iostream>
template <class T, int N>
   void tester() {
      using namespace std;
      vector<Wrap<T>> v;
      enum { NB = traits_taille<T,N>::value };
      v.reserve(NB);
      generate_n(back_inserter(v), int{NB}, creer_wrap<T>);
      tester(clog, v);
   }

Étant donné qu'il n'est pas possible d'appliquer une approche itérative sur une liste de types, nous procéderons par une approche récursive, un type à la fois, à l'aide du type appliquer.

Ce type appliquera la méthode (générique) execute() d'une classe oper (du type Op) donnée ur chaque type d'une liste de types. Il est hautement probable que le type Op ici soit un foncteur.

Notez que cette partie de l'exemple devrait être modernisée pour s'exprimer sous forme de Fold Expression. Je le ferai quand j'aurai quelques minutes...

template <class>
   struct appliquer;
template <class T, class Q>
   struct appliquer<type_list<T,Q>> {
      template <class Op>
         static void execute(Op oper) {
            oper.execute<T>();
            appliquer<Q>::execute(oper);
         }
   };
template <class T>
   struct appliquer<type_list<T,Vide>> {
      template <class Op>
         static void execute(Op oper) {
            oper.execute<T>();
         }
   };

Le démarage des tests passera par la classe tests<N>, à travers la méthode execute<T>(). Le nombre de tests sera donc fixé au global, alors que la réalisation de chaque test sera typée.

template <int N>
   struct tests {
      template<class T>
         void execute() {
            tester<T,N>();
         }
   };

Pour répondre en partie à la question initiale, en lien avec un agrégat de nombres à virgule flottante, nous utiliserons aussi les types point2D et point3D, qui rejoindront conceptuellement et structurellement les instances de TiVecteur dans les tests initiaux.

J'ai adapté les outils de création d'objets (fonctions génératrices creer_wrap<T>()) à ces types, pour que le code de test demeure aussi simple que possible.

Le recours à un opérateur + pour chaque type de point tient au fait que cet opérateur est utilisé par nos implémentations des opérations suppléées à std:accumulate() dans le code de test.

struct point2D {
   float x{}, y{};
   point2D() = default;
   point2D(float x, float y) : x{x}, y{y} {
   }
};
point2D operator+(point2D a, point2D b) {
   return { a.x + b.x, a.y + b.y };
}
std::ostream& operator<<(std::ostream &os, const point2D &pt) {
   return os << pt.x << ',' << pt.y;
}
template <>
   Wrap<point2D> creer_wrap<point2D>() {
      using std::rand;
      auto creer = [] {
         return static_cast<float>(rand()) / RAND_MAX;
      };
      return { point2D{ creer(), creer() } };
   }

struct point3D {
   float x{}, y{}, z{};
   point3D() = default;
   point3D(float x, float y, float z) : x{x}, y{y}, z{z} {
   }
};
point3D operator+(point3D a, point3D b) {
   return {a.x + b.x, a.y + b.y, a.z + b.z};
}
std::ostream& operator<<(std::ostream &os, const point3D &pt) {
   return os << pt.x << ',' << pt.y << ',' << pt.z;
}
template <>
   Wrap<point3D> creer_wrap<point3D>() {
      using std::rand;
      auto creer = [] {
         return static_cast<float>(rand()) / RAND_MAX;
      };
      return { point3D{ creer(), creer(), creer() } };
   }

Le programme principal, enfin, applique le même nombre de tests (a priori, du moins, donc avant adaptation en fonction de la complexité structurelle du type visé) aux types de la liste de types nommée localement liste.

#include <string>
int main()
{
   using namespace std;
   nullout.open("dev_null");
   ofstream out{"out.txt"};
   clog.rdbuf(out.rdbuf());
   using liste = MAKE_TLIST_9(
      double, char, short, int, long, float, string, point2D, point3D, double
   );
   appliquer<liste>::execute(tests<40000000>());
   clog.rdbuf(nullptr);
}

À l'exécution, sur mon petit ordinateur portatif, j'obtiens :

Type double, sizeof(double): 8, sizeof(Wrap<double>): 8, 40000000 tests
	Wrap<double>, par valeur: 390.916 ms.
	Wrap<double>, par ref-to-const: 82.9237 ms.
	double, par valeur: 78.0677 ms.
	double, par ref-to-const: 76.9429 ms.
Type struct point3D, sizeof(struct point3D): 12, sizeof(Wrap<struct point3D>): 12, 40000000 tests
	Wrap<struct point3D>, par valeur: 409.007 ms.
	Wrap<struct point3D>, par ref-to-const: 413.761 ms.
	struct point3D, par valeur: 416.922 ms.
	struct point3D, par ref-to-const: 445.863 ms.
Type struct point2D, sizeof(struct point2D): 8, sizeof(Wrap<struct point2D>): 8, 40000000 tests
	Wrap<struct point2D>, par valeur: 329.589 ms.
	Wrap<struct point2D>, par ref-to-const: 351.761 ms.
	struct point2D, par valeur: 330.858 ms.
	struct point2D, par ref-to-const: 333.001 ms.
Type string, sizeof(string): 28, sizeof(Wrap<string>): 28, 80000 tests
	Wrap<string>, par valeur: 13324.5 ms.
	Wrap<string>, par ref-to-const: 4688.57 ms.
	string, par valeur: 5024 ms.
	string, par ref-to-const: 4637.68 ms.
Type float, sizeof(float): 4, sizeof(Wrap<float>): 4, 40000000 tests
	Wrap<float>, par valeur: 219.235 ms.
	Wrap<float>, par ref-to-const: 215.845 ms.
	float, par valeur: 216.211 ms.
	float, par ref-to-const: 217.646 ms.
Type long, sizeof(long): 4, sizeof(Wrap<long>): 4, 40000000 tests
	Wrap<long>, par valeur: 42.146 ms.
	Wrap<long>, par ref-to-const: 41.2557 ms.
	long, par valeur: 40.7634 ms.
	long, par ref-to-const: 41.2694 ms.
Type int, sizeof(int): 4, sizeof(Wrap<int>): 4, 40000000 tests
	Wrap<int>, par valeur: 40.0836 ms.
	Wrap<int>, par ref-to-const: 40.1995 ms.
	int, par valeur: 67.3221 ms.
	int, par ref-to-const: 39.4935 ms.
Type short, sizeof(short): 2, sizeof(Wrap<short>): 2, 40000000 tests
	Wrap<short>, par valeur: 32.8086 ms.
	Wrap<short>, par ref-to-const: 50.0433 ms.
	short, par valeur: 30.3995 ms.
	short, par ref-to-const: 30.1335 ms.
Type char, sizeof(char): 1, sizeof(Wrap<char>): 1, 40000000 tests
	Wrap<char>, par valeur: 27.9922 ms.
	Wrap<char>, par ref-to-const: 28.09 ms.
	char, par valeur: 28.1164 ms.
	char, par ref-to-const: 39.2361 ms.
Type double, sizeof(double): 8, sizeof(Wrap<double>): 8, 40000000 tests
	Wrap<double>, par valeur: 393.712 ms.
	Wrap<double>, par ref-to-const: 82.6255 ms.
	double, par valeur: 95.9937 ms.
	double, par ref-to-const: 82.3362 ms.

Sous forme tabulaire, on trouve donc :

T
double
char
short
int
long
float
std::string
point2D
point3D
double
Wrap<T>
393,712 ms.
27,9922 ms.
32,8086 ms.
40,0836 ms.
42,146 ms.
219,235 ms.
13324,5 ms.
329,589 ms.
409,007 ms.
390,916 ms.
const Wrap<T>&
82,6255 ms.
28,09 ms.
50,0433 ms.
40,1995 ms.
41,2557 ms.
215,845 ms.
4688,57 ms.
351,761 ms.
413,761 ms.
82,9237 ms.
T
95,9937 ms.
28,1164 ms.
30,3995 ms.
67,3221 ms.
40,7634 ms.
216,211 ms.
5024 ms.
330,858 ms.
416,922 ms.
78,0877 ms.
const T&
82,3362 ms.
39,2361 ms.
30,1335 ms.
39,4935 ms.
41,2694 ms.
217,646 ms.
4637,68 ms.
333,001 ms.
445,863 ms.
76,9429 ms.
sizeof(T)
8
1
2
4
4
4
28
8
12
8
sizeof(Wrap<T>)
8
1
2
4
4
4
28
8
12
8
N
40000000
40000000
40000000
40000000
40000000
40000000
80000
40000000
40000000
40000000

Quelques remarques :

Dans le doute, faites vos propres tests!

Lectures complémentaires

Quelques liens pour enrichir votre vision du sujet.


Valid XHTML 1.0 Transitional

CSS Valide !