Empêcher les optimisations indûes

Ce qui suit résulte d'une question posée par Guillaume Paquette-Boisclair, étudiant du cours IFT729 à l'Université de Sherbrooke à l'hiver 2012. J'y utilise std::rand() pour fins de démonstration, mais cette fonction est dépréciée (voir ../Divers--cplusplus/prng.htmlpour savoir quoi utiliser en pratique)

L'optimisateur d'un compilateur C++ tend à être extrêmement agressif et extrêmement « performant », allant jusqu'à supprimer du code qu'il juge inutile. En général, est inutile du code qui n'a pas d'effet sur le reste du programme ou qui n'a pas d'effets secondaires.

On peut d'ailleurs constater cette action assez facilement. Supposons du code comme celui-ci :

// inclusions et using...
template <class T>
   void test_vecteur(vector<T> &v, int n)
   {
      generate_n(back_inserter(v), n, rand());
      sort(begin(v), end(v));
   }
template <class F, class C>
   void tester(F fct, C &conteneur, int n)
{
   auto debut = system_clock::now();
   fct(conteneur, n);
   auto fin = system_clock::now();
   cout << "Temps ecoule pour generer et trier " << n << " elements de ce conteneur: "
        << duration_cast<milliseconds>(fin - debut).count() << " ms. " << endl;
}
int main()
{
   for (int i = 1; i < 1000000; i *= 10)
   {
      vector<int> v;
      tester(test_vecteur, v, i);
   }
}

Si ce code est compilé en mode Debug, le temps requis pour trier n valeur croîtra (lentement) avec la valeur de n. En mode Release, par contre, il est hautement probable que le temps soit toujours zéro, peu importe la qualité des outils de mesure choisis.

La raison est simple : le code menant à la génération de n valeurs pseudo-aléatoires et le tri subséquent se font sur des données qui ne sont jamais utilisées. Le compilateur le constate sans peine et, simplement, s'en débarrasse. Pourquoi faire des choses inutiles, après tout?

Évidemment, pour qui souhaite tester la performance de generate_n(), back_inserter() ou sort(), c'est un peu agaçant. Ce l'on veut alors, pour s'assurer que le code soit absolument généré, est faire en sorte que son résultat ait un effet (aux yeux du compilateur). Par exemple, ceci :

int cumul = 0;
for(int i = 0; i < 10; ++i)
   cumul += rand() % (i+1); // le +1 est là pour éviter les divisions par zéro

,,,sera retiré par l'optimisateur si cumul n'est pas utilisé par la suite. De même, si on ajoute ceci :

int cumul = 0;
for(int i = 0; i < 10; ++i)
   cumul += rand() % (i+1);
int haha = cumul + 1; // bof

...cela ne nous avancera pas, car bien qu'on ait utilisé cumul, il se peut que haha ne serve pas, et l'optimiseur va s'en apercevoir, évidemment. Par contre, nous faisons une entrée/sortie, c'est une autre histoire :

int cumul = 0;
for(int i = 0; i < 10; ++i)
   cumul += rand() % (i+1);
cout << cumul; // bingo!

...car le fait d'afficher le résultat rend ce code pertinent (qu'il le soit ou non). Si nous ne souhaitons pas engluer notre code d'affichages bidons, il est possible (a) de les écrire sur un flux ou (b) de les envoyer sur /dev/null ou l'équivalent :

#include <iostream>
using std::clog;
int main()
{
   clog.rdbuf(nullptr); // car par défaut, clog écrit sur stdout comme cout
   int cumul = 0;
   for(int i = 0; i < 10; ++i)
      cumul += rand() % (i+1);
   clog << cumul; // bingo!
}

Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !