Ces optimisations, faites par tout compilateur qui se respecte, sont vraiment essentielles à connaitre. Leur meilleur connaissance permettrait à beaucoup de programmeurs de réaliser que pour des raisons de performance, il est important de renvoyer les objets par valeur.
On voit malheureusement encore beaucoup de personnes écrire des signatures comme :
voidf(constT&input,U&output);
lorsque U est un objet un peu gros comme un std::vector ou un std::array. Souvent l'argument donné est que cela permet d'éviter d'avoir à faire une copie. Mais comme l'explique très bien cet article, c'est inutile car le compilateur peut (en respectant une ou deux règles simples) faire un court-circuit de la copie. Et il n'y a pas besoin de move semantics pour que cela soit efficace, donc n'hesitez pas à renvoyer par valeur des std::array. En conclusion, utilisez plutôt la signature :
Uf(constT&input)
Cette signature permet de plus aux compilateurs de nombreuses optimisations impossibles avec la première signature. Prenez par exemple cette fonction:
Elle ne peut pas être optimisée pour son usage courant. En effet, il faut savoir qu'on peut calculer très efficacement le cos et le sin d'un même angle (avec une seule instruction x86). Cela semble être le moment idéal pour utiliser cette instruction. Malheureusement, c'est impossible, car si on appelle f(x, x), on voit bien que la première ligne change x[0] et donc que les deux angles ne sont pas les mêmes. Si par contre on utilise la signature:
std::array<double,2>f(conststd::array<double>&x)
le compilateur peut faire l'optimisation et le code sera 2 fois plus rapide. Essayez d'écrire les deux fonctions et regardez l'assembleur produit (ou mieux mesurez le temps d'execution) et vous verrez qu'avec un bon compilateur (le compilo intel, ou une version récente de gcc), la seconde signature est beaucoup plus efficace. De nombreuses librairies, comme Boost::odeint limitent fortement leur performance car elles imposent la première signature.
De manière générale, l'aliasing induit par ces "retour par référence" peut être catastrophique pour la performance. J'ai réussit une fois à faire une x3 en performance rien qu'en changeant une telle signature. Comme il n'y avait plus d'aliasing sur des std::array, le compilateur a pu tout reordonner les calculs et pu profiter à fond du parallélisme d'instructions.
Bref, pour écrire des codes performants, renvoyez par valeur. Chandler Carruth, qui s'occupe de l'optimiseur de LLVM est un grand militant pour cela. Regardez ses conférences qui sont très instructives.
Enfin, afin de donner un contre exemple, il est quand même parfois intéressant d'avoir un retour par référence. comme sur ce code:
std::stringline;while(file.get_line(line)){// process line}
Comme les fichiers ont souvent des lignes de moins de 80 (ou 120) caractères, il va y avoir une allocation dynamique de mémoire au début de la boucle, puis plus aucune ensuite, sauf si la ligne grossit, mais c'est rare. Ma politique est de toujours renvoyer par valeurs, sauf si vous avez un benchmark clair qui montre que on est dans ce type de cas.
Au passage, je me permet de faire la pub pour un projet libre perso : des conteneurs efficaces pour le calcul haute performance. C'est ici : https://github.com/insideloop/InsideLoop
# Value Semantics Rocks
Posté par magnolia . En réponse à la dépêche C++17 garantit le court-circuit de copie (suite de la précédente dépêche). Évalué à 7.
Ces optimisations, faites par tout compilateur qui se respecte, sont vraiment essentielles à connaitre. Leur meilleur connaissance permettrait à beaucoup de programmeurs de réaliser que pour des raisons de performance, il est important de renvoyer les objets par valeur.
On voit malheureusement encore beaucoup de personnes écrire des signatures comme :
lorsque U est un objet un peu gros comme un std::vector ou un std::array. Souvent l'argument donné est que cela permet d'éviter d'avoir à faire une copie. Mais comme l'explique très bien cet article, c'est inutile car le compilateur peut (en respectant une ou deux règles simples) faire un court-circuit de la copie. Et il n'y a pas besoin de move semantics pour que cela soit efficace, donc n'hesitez pas à renvoyer par valeur des std::array. En conclusion, utilisez plutôt la signature :
Cette signature permet de plus aux compilateurs de nombreuses optimisations impossibles avec la première signature. Prenez par exemple cette fonction:
Elle ne peut pas être optimisée pour son usage courant. En effet, il faut savoir qu'on peut calculer très efficacement le cos et le sin d'un même angle (avec une seule instruction x86). Cela semble être le moment idéal pour utiliser cette instruction. Malheureusement, c'est impossible, car si on appelle f(x, x), on voit bien que la première ligne change x[0] et donc que les deux angles ne sont pas les mêmes. Si par contre on utilise la signature:
le compilateur peut faire l'optimisation et le code sera 2 fois plus rapide. Essayez d'écrire les deux fonctions et regardez l'assembleur produit (ou mieux mesurez le temps d'execution) et vous verrez qu'avec un bon compilateur (le compilo intel, ou une version récente de gcc), la seconde signature est beaucoup plus efficace. De nombreuses librairies, comme Boost::odeint limitent fortement leur performance car elles imposent la première signature.
De manière générale, l'aliasing induit par ces "retour par référence" peut être catastrophique pour la performance. J'ai réussit une fois à faire une x3 en performance rien qu'en changeant une telle signature. Comme il n'y avait plus d'aliasing sur des std::array, le compilateur a pu tout reordonner les calculs et pu profiter à fond du parallélisme d'instructions.
Bref, pour écrire des codes performants, renvoyez par valeur. Chandler Carruth, qui s'occupe de l'optimiseur de LLVM est un grand militant pour cela. Regardez ses conférences qui sont très instructives.
Enfin, afin de donner un contre exemple, il est quand même parfois intéressant d'avoir un retour par référence. comme sur ce code:
Comme les fichiers ont souvent des lignes de moins de 80 (ou 120) caractères, il va y avoir une allocation dynamique de mémoire au début de la boucle, puis plus aucune ensuite, sauf si la ligne grossit, mais c'est rare. Ma politique est de toujours renvoyer par valeurs, sauf si vous avez un benchmark clair qui montre que on est dans ce type de cas.
Au passage, je me permet de faire la pub pour un projet libre perso : des conteneurs efficaces pour le calcul haute performance. C'est ici : https://github.com/insideloop/InsideLoop