Pourquoi les lambdas peuvent-elles être mieux optimisées par le compilateur que les fonctions simples?

171

Dans son livre, The C++ Standard Library (Second Edition)Nicolai Josuttis déclare que les lambdas peuvent être mieux optimisées par le compilateur que les fonctions simples.

De plus, les compilateurs C ++ optimisent les lambdas mieux que les fonctions ordinaires. (Page 213)

Pourquoi donc?

Je pensais qu'en matière d'inlining, il ne devrait plus y avoir de différence. La seule raison à laquelle je pourrais penser est que les compilateurs pourraient avoir un meilleur contexte local avec les lambdas et que ceux-ci peuvent faire plus d'hypothèses et effectuer plus d'optimisations.

Stéphan Dollberg
la source
Connexes .
iammilind
Fondamentalement, l'instruction s'applique à tous les objets de fonction , pas seulement aux lambdas.
newacct
4
Ce serait incorrect car les pointeurs de fonction sont également des objets de fonction.
Johannes Schaub - litb
2
@litb: Je pense que je ne suis pas d'accord avec cela. ^ W ^ W ^ W ^ W ^ W ^ W (après avoir examiné la norme) Je n'étais pas au courant de ce C ++ ism, même si je pense que dans le langage courant (et selon wikipedia), les gens veulent dire instance-of-some-callable-class quand ils disent objet fonction.
Sebastian Mach
1
Certains compilateurs peuvent mieux optimiser les lambdas que les fonctions simples, mais pas tous :-(
Cody Gray

Réponses:

175

La raison en est que les lambdas sont des objets de fonction, donc les transmettre à un modèle de fonction instanciera une nouvelle fonction spécifiquement pour cet objet. Le compilateur peut ainsi intégrer de manière triviale l'appel lambda.

Pour les fonctions, en revanche, l'ancienne mise en garde s'applique: un pointeur de fonction est passé au modèle de fonction, et les compilateurs ont traditionnellement beaucoup de problèmes pour insérer des appels via des pointeurs de fonction. Ils peuvent théoriquement être insérés, mais uniquement si la fonction environnante est également incorporée.

À titre d'exemple, considérons le modèle de fonction suivant:

template <typename Iter, typename F>
void map(Iter begin, Iter end, F f) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

Appelez-le avec un lambda comme ceci:

int a[] = { 1, 2, 3, 4 };
map(begin(a), end(a), [](int n) { return n * 2; });

Résultats dans cette instanciation (créée par le compilateur):

template <>
void map<int*, _some_lambda_type>(int* begin, int* end, _some_lambda_type f) {
    for (; begin != end; ++begin)
        *begin = f.operator()(*begin);
}

… Le compilateur sait _some_lambda_type::operator ()et peut les appeler en ligne de manière triviale. (Et appeler la fonction mapavec n'importe quel autre lambda créerait une nouvelle instanciation de mappuisque chaque lambda a un type distinct.)

Mais lorsqu'elle est appelée avec un pointeur de fonction, l'instanciation se présente comme suit:

template <>
void map<int*, int (*)(int)>(int* begin, int* end, int (*f)(int)) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

… Et fpointe ici vers une adresse différente pour chaque appel à mapet donc le compilateur ne peut pas appeler en ligne à fmoins que l'appel environnant à mapn'ait également été inséré afin que le compilateur puisse résoudre fune fonction spécifique.

Konrad Rudolph
la source
4
Peut-être vaut-il la peine de mentionner que l'instanciation du même modèle de fonction avec une expression lambda différente créera une toute nouvelle fonction avec un type unique, ce qui pourrait bien être un inconvénient.
détendre
2
@greggo Absolument. Le problème est de gérer des fonctions qui ne peuvent pas être insérées (car elles sont trop volumineuses). Ici, l'appel au callback peut toujours être intégré dans le cas d'un lambda, mais pas dans le cas d'un pointeur de fonction. std::sortest l'exemple classique de ceci en utilisant des lambdas au lieu d'un pointeur de fonction ici apporte jusqu'à sept fois (probablement plus, mais je n'ai pas de données à ce sujet!) les performances augmentent.
Konrad Rudolph
1
@greggo Vous confondez deux fonctions ici: celle que nous passer le lambda à (par exemple std::sort, ou mapdans mon exemple) et le lambda lui - même. Le lambda est généralement petit. L'autre fonction - pas nécessairement. Nous sommes concernés par les appels en ligne au lambda à l'intérieur de l'autre fonction.
Konrad Rudolph
2
@greggo je sais. C'est littéralement ce que dit la dernière phrase de ma réponse.
Konrad Rudolph
1
Ce que je trouve curieux (après avoir juste trébuché dessus), c'est que, étant donné une simple fonction booléenne preddont la définition est visible, et en utilisant gcc v5.3, std::find_if(b, e, pred)ne se met pas en ligne pred, mais le std::find_if(b, e, [](int x){return pred(x);})fait. Clang parvient à intégrer les deux, mais ne produit pas de code aussi vite que g ++ avec le lambda.
rici
26

Parce que lorsque vous passez une "fonction" à un algorithme, vous passez en fait un pointeur vers la fonction, il doit donc faire un appel indirect via le pointeur vers la fonction. Lorsque vous utilisez un lambda, vous passez un objet à une instance de modèle spécialement instanciée pour ce type et l'appel à la fonction lambda est un appel direct, pas un appel via un pointeur de fonction, il est donc beaucoup plus probable qu'il soit en ligne.

jcoder
la source
5
"l'appel à la fonction lambda est un appel direct" - en effet. Et la même chose est vraie pour tous les objets de fonction, pas seulement pour les lambdas. Ce ne sont que des pointeurs de fonction qui ne peuvent pas être intégrés aussi facilement, voire pas du tout.
Pete Becker