Julien Jorge's Personal Website

Constexpr versus template

Fri Apr 23, 2021

Ce post a été publié sur LinuxFr.org. Vous pouvez le lire là bas, ainsi que les commentaires associés

Bonjour ’nal,

J’étais tranquille, en train de m’occuper de mes propres affaires, quand soudain je me suis demandé si l’utilisation de constexpr introduit dans C++11 pouvait réduire les temps de compilation par rapport à la méthode précédente de la métaprogrammation via des templates.

Pour rappel, l’idée de constexpr est d’indiquer au compilateur que la fonction ou variable concernée peut être calculée à la compilation si tous ses paramètres sont connus, et doit être calculée ainsi si elle apparaît dans un contexte d’expression constante (par exemple un paramètre de template).

Prenons cette chouette fonction pour illustrer :

// Calcule n en additionnant des 1 n fois.
// CC-BY-SA-NC-ND, patent pending.
unsigned count(unsigned n)
{
  return (n == 0) ? 0 : 1 + count(n - 1);
}

Si je voulais utiliser cette fonction dans un contexte où une constante de compilation est nécessaire, en C++98 je ne pourrais pas:

// count() est compilée et évaluée à l'exécution, ce qui est une
// erreur: les tableaux de taille variable ne sont pas conformes
// au standard.
int array[count(24)];

Pour contourner cela une solution est faire le calcul dans un type, via de la métaprogrammation avec templates:

template<unsigned N>
struct count_t;

// le cas d'arrêt
template<>
struct count_t<0>
{
  enum { value = 0 };
};

// le calcul récursif pour le cas général.
template<unsigned N>
struct count_t
{
  enum
    {
      value = 1 + count_t<N - 1>::value
    };
};

Et là tu vas me dire que c’est pas très DRY. Je me retrouve avec deux implémentations du même calcul, l’un est pour être effectué à la compilation et l’autre à l’exécution. Cela ne serait-il pas mieux si la même implémentation pouvait être utilisée pour les deux cas ?

constexpr enters the chat.

En C++11 il suffit de déclarer la fonction avec le mot clé constexpr pour qu’elle soit utilisable à la compilation (en gros hein, je simplifie pour l’exemple) :

constexpr int count(unsigned n)
{
  return (n == 0) ? 0 : 1 + count(n - 1);
}

void foo()
{
  // Pouf ! Ça marche.
  int array[count(24)];
}

Accessoirement il me semble que cette possibilité a aussi l’avantage de contourner la limite d’instanciations récursives des templates imposée par le compilateur.

Revenons à l’interrogation initiale. Tu n’es pas sans ignorer que la métaprogrammation à base de templates met à genoux les compilateurs. C’est la 3è plus grosse consommation d’électricité et plus gros producteur de CO2 depuis 1876. Est-ce que la version constexpr est aussi coûteuse ?

Pour comparer tout ça j’ai lancé la compilation des exemples ci-dessus en boucle, de 50 à 500 fois, et j’ai mesuré le temps que prenait la boucle, en secondes. Le tout avec G++ 9.3. Au final ça donne ça:

# nombre de compilations, templates (s.), constexpr (s.)
50,                       1.193,          0.596
100,                      2.218,          1.252
150,                      3.449,          1.763
200,                      4.628,          2.358
250,                      5.975,          2.984
300,                      7.022,          3.484
350,                      8.067,          4.132
400,                      9.237,          4.870
450,                      10.300,         5.344
500,                      11.410,         5.835

Voilà, ça compile à peu près deux fois plus rapidement en utilisant la version constexpr que la version templates. À titre de comparaison, compiler 500 fois un fichier vide prend environ 4 secondes.