Implémentation lambda C ++ 11 et modèle de mémoire

92

J'aimerais avoir des informations sur la façon de penser correctement les fermetures C ++ 11 et std::functionen termes de mise en œuvre et de gestion de la mémoire.

Bien que je ne crois pas à l'optimisation prématurée, j'ai l'habitude de considérer attentivement l'impact de mes choix sur les performances lors de l'écriture de nouveau code. Je fais également une bonne quantité de programmation en temps réel, par exemple sur des microcontrôleurs et pour des systèmes audio, où les pauses d'allocation / désallocation de mémoire non déterministes doivent être évitées.

Par conséquent, j'aimerais développer une meilleure compréhension du moment où utiliser ou non les lambdas C ++.

Ma compréhension actuelle est qu'un lambda sans fermeture capturée est exactement comme un rappel C. Cependant, lorsque l'environnement est capturé par valeur ou par référence, un objet anonyme est créé sur la pile. Quand une valeur-fermeture doit être retournée à partir d'une fonction, on l'enveloppe std::function. Qu'arrive-t-il à la mémoire de fermeture dans ce cas? Est-il copié de la pile vers le tas? Est-il libéré chaque fois que le std::functionest libéré, c'est-à-dire est-il compté comme un std::shared_ptr?

J'imagine que dans un système en temps réel, je pourrais mettre en place une chaîne de fonctions lambda, en passant B comme argument de continuation à A, afin qu'un pipeline de traitement A->Bsoit créé. Dans ce cas, les fermetures A et B seraient attribuées une fois. Bien que je ne sache pas si ceux-ci seraient alloués sur la pile ou sur le tas. Cependant, en général, cela semble sûr à utiliser dans un système en temps réel. D'un autre côté, si B construit une fonction lambda C, qu'il renvoie, alors la mémoire de C serait allouée et désallouée à plusieurs reprises, ce qui ne serait pas acceptable pour une utilisation en temps réel.

En pseudo-code, une boucle DSP, qui, je pense, va être sûre en temps réel. Je veux exécuter le bloc de traitement A puis B, où A appelle son argument. Ces deux fonctions renvoient des std::functionobjets, il en fsera de même pour un std::functionobjet, où son environnement est stocké sur le tas:

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

Et un qui, je pense, pourrait être mauvais à utiliser dans le code en temps réel:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

Et celui où je pense que la mémoire de pile est probablement utilisée pour la fermeture:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

Dans ce dernier cas, la fermeture est construite à chaque itération de la boucle, mais contrairement à l'exemple précédent, elle est bon marché car elle ressemble à un appel de fonction, aucune allocation de tas n'est faite. De plus, je me demande si un compilateur pourrait "lever" la fermeture et faire des optimisations en ligne.

Est-ce correct? Je vous remercie.

Steve
la source
4
Il n'y a pas de surcharge lors de l'utilisation d'une expression lambda. L'autre choix serait d'écrire vous-même un tel objet fonction, ce qui serait exactement le même. Btw, sur la question en ligne, puisque le compilateur a toutes les informations dont il a besoin, il peut certainement simplement insérer l'appel au operator(). Il n'y a pas de "levage" à faire, les lambdas n'ont rien de spécial. Ils ne sont qu'un raccourci pour un objet de fonction local.
Xeo
Cela semble être une question de savoir si std::functionstocke son état sur le tas ou non, et n'a rien à voir avec les lambdas. Est-ce correct?
Mooing Duck
8
Juste pour le préciser en cas de malentendu: une expression lambda n'est pas un std::function!!
Xeo
1
Juste un petit commentaire: soyez prudent lorsque vous renvoyez un lambda depuis une fonction, car toutes les variables locales capturées par référence deviennent invalides après avoir quitté la fonction qui a créé le lambda.
Giorgio
2
@Steve depuis C ++ 14, vous pouvez renvoyer un lambda à partir d'une fonction avec un autotype de retour.
Oktalist

Réponses:

100

Ma compréhension actuelle est qu'un lambda sans fermeture capturée est exactement comme un rappel C. Cependant, lorsque l'environnement est capturé par valeur ou par référence, un objet anonyme est créé sur la pile.

Non; il s'agit toujours d' un objet C ++ de type inconnu, créé sur la pile. Un lambda sans capture peut être converti en un pointeur de fonction (bien qu'il soit adapté aux conventions d'appel C dépend de l'implémentation), mais cela ne signifie pas qu'il s'agit d' un pointeur de fonction.

Lorsqu'une valeur-fermeture doit être retournée à partir d'une fonction, on l'encapsule dans std :: function. Qu'arrive-t-il à la mémoire de fermeture dans ce cas?

Un lambda n'a rien de spécial en C ++ 11. C'est un objet comme tout autre objet. Une expression lambda entraîne un temporaire, qui peut être utilisé pour initialiser une variable sur la pile:

auto lamb = []() {return 5;};

lambest un objet de pile. Il a un constructeur et un destructeur. Et il suivra toutes les règles C ++ pour cela. Le type de lambcontiendra les valeurs / références capturées; ils seront membres de cet objet, comme tout autre membre d'objet de tout autre type.

Vous pouvez le donner à un std::function:

auto func_lamb = std::function<int()>(lamb);

Dans ce cas, il obtiendra une copie de la valeur de lamb. Si lambavait capturé quoi que ce soit par valeur, il y aurait deux copies de ces valeurs; un dans lambet un dans func_lamb.

Lorsque la portée actuelle se termine, func_lambsera détruite, suivie de lamb, conformément aux règles de nettoyage des variables de pile.

Vous pouvez tout aussi bien en allouer un sur le tas:

auto func_lamb_ptr = new std::function<int()>(lamb);

L'endroit exact où la mémoire pour le contenu d'un std::functionva dépend de l'implémentation, mais l'effacement de type utilisé par std::functionnécessite généralement au moins une allocation de mémoire. C'est pourquoi std::functionle constructeur de peut prendre un allocateur.

Est-il libéré à chaque fois que la fonction std :: est libérée, c'est-à-dire est-elle comptée comme un std :: shared_ptr?

std::functionstocke une copie de son contenu. Comme pratiquement tous les types de bibliothèques standard C ++, functionutilise la sémantique des valeurs . Ainsi, il est copiable; lorsqu'il est copié, le nouvel functionobjet est complètement séparé. Il est également mobile, de sorte que toutes les allocations internes peuvent être transférées de manière appropriée sans avoir besoin de plus d'allocation et de copie.

Il n'est donc pas nécessaire de compter les références.

Tout le reste que vous déclarez est correct, en supposant que «allocation de mémoire» équivaut à «mauvaise à utiliser dans le code en temps réel».

Nicol Bolas
la source
1
Excellente explication, merci. Ainsi, la création de std::functionest le point auquel la mémoire est allouée et copiée. Il semble s'ensuivre qu'il n'y a aucun moyen de retourner une fermeture (puisqu'elles sont allouées sur la pile), sans d'abord copier dans a std::function, oui?
Steve
3
@Steve: Oui; vous devez envelopper un lambda dans une sorte de conteneur pour qu'il quitte la portée.
Nicol Bolas
Le code de la fonction entière est-il copié ou la fonction d'origine est-elle allouée au moment de la compilation et transmise les valeurs fermées?
Llamageddon
Je veux ajouter que la norme impose plus ou moins indirectement (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) que si un lambda ne capture rien, il peut être stocké dans un std::functionobjet sans mémoire dynamique allocation en cours.
5gon12eder
2
@Yakk: Comment définissez-vous «grand»? Un objet avec deux pointeurs d'état est-il "grand"? Que diriez-vous de 3 ou 4? De plus, la taille de l'objet n'est pas le seul problème; si l'objet n'est pas mobile, il doit être stocké dans une allocation, car il functiona un constructeur de mouvement noexcept. Tout l'intérêt de dire «exige généralement» est que je ne dis pas « exige toujours »: qu'il y a des circonstances où aucune allocation ne sera effectuée.
Nicol Bolas
0

C ++ lambda est juste un sucre syntaxique autour de la classe Functor (anonyme) avec surchargé operator()et std::functionest juste un wrapper autour des callables (c'est-à-dire des foncteurs, des lambdas, des fonctions c, ...) qui copie par valeur l '"objet lambda solide" du courant portée de la pile - au tas .

Pour tester le nombre de constructeurs / relocatons réels, j'ai fait un test (en utilisant un autre niveau de wrapping vers shared_ptr mais ce n'est pas le cas). Voir par vous-même:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

il fait cette sortie:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

Exactement le même ensemble de ctors / dtors serait appelé pour l'objet lambda alloué à la pile! (Maintenant, il appelle Ctor pour l'allocation de pile, Copy-ctor (+ allocation de tas) pour le construire dans std :: function et un autre pour faire l'allocation de tas shared_ptr + construction de la fonction)

Barney
la source