web-dev-qa-db-fra.com

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

Je voudrais des informations sur la façon de penser correctement aux fermetures C++ 11 et std::function en termes de mise en œuvre et de gestion de la mémoire.

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

Par conséquent, je voudrais développer une meilleure compréhension du moment d'utiliser ou de ne pas utiliser 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. Lorsqu'une valeur-fermeture doit être retournée à partir d'une fonction, on l'enveloppe dans std::function. Qu'advient-il de 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::function est libéré, c'est-à-dire qu'il est 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, de sorte qu'un pipeline de traitement A->B est créé. Dans ce cas, les fermetures A et B seraient attribuées une fois. Bien que je ne sois pas sûr que ceux-ci seraient alloués sur la pile ou 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'elle 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, sera 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 std::function objets, donc f sera un std::function objet, 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 je pense que cela 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 la 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 effectuée. De plus, je me demande si un compilateur pourrait "lever" la fermeture et faire des optimisations en ligne.

Est-ce correct? Merci.

87
Steve

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; c'est toujours 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 que cela est a pointeur de fonction.

Lorsqu'une valeur-fermeture doit être retournée à partir d'une fonction, on l'enveloppe dans std :: function. Qu'advient-il de 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 résulte en un temporaire, qui peut être utilisé pour initialiser une variable sur la pile:

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

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

Vous pouvez le donner à un std::function:

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

Dans ce cas, il obtiendra copie de la valeur de lamb. Si lamb avait capturé quelque chose par valeur, il y aurait deux copies de ces valeurs; un dans lamb et un dans func_lamb.

Lorsque la portée actuelle se termine, func_lamb Sera détruit, suivi 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);

La destination exacte de la mémoire du contenu d'un std::function Dépend de l'implémentation, mais l'effacement de type utilisé par std::function Nécessite généralement au moins une allocation de mémoire. C'est pourquoi le constructeur de std::function Peut prendre un allocateur.

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

std::function Stocke une copie de son contenu. Comme pratiquement tous les types de bibliothèque C++ standard, function utilise valeur sémantique. Ainsi, il est copiable; lorsqu'il est copié, le nouvel objet function 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 nécessiter 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 "l'allocation de mémoire" équivaut à "mauvais à utiliser dans le code en temps réel".

93
Nicol Bolas

C++ lambda est juste un sucre syntaxique autour de la classe Functor (anonyme) avec une surcharge de operator() et std::function est juste un wrapper autour des callables (c'est-à-dire des foncteurs, des lambdas, des fonctions c, ...) qui fait copier par valeur "l'objet lambda solide" de l'étendue de la pile actuelle - vers le - tas.

Pour tester le nombre de constructeurs/relocatons réels, j'ai fait un test (en utilisant un autre niveau de wrapping pour 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)

0
barney