J'aimerais avoir des informations sur la façon de penser correctement les fermetures C ++ 11 et std::function
en 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::function
est 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->B
soit 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::function
objets, il en f
sera de même pour 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 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.
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.std::function
stocke son état sur le tas ou non, et n'a rien à voir avec les lambdas. Est-ce correct?std::function
!!auto
type de retour.Réponses:
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.
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:
lamb
est un objet de pile. Il a un constructeur et un destructeur. Et il suivra toutes les règles C ++ pour cela. Le type delamb
contiendra 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
:Dans ce cas, il obtiendra une copie de la valeur de
lamb
. Silamb
avait capturé quoi que ce soit par valeur, il y aurait deux copies de ces valeurs; un danslamb
et un dansfunc_lamb
.Lorsque la portée actuelle se termine,
func_lamb
sera détruite, suivie delamb
, conformément aux règles de nettoyage des variables de pile.Vous pouvez tout aussi bien en allouer un sur le tas:
L'endroit exact où la mémoire pour le contenu d'un
std::function
va dépend de l'implémentation, mais l'effacement de type utilisé parstd::function
nécessite généralement au moins une allocation de mémoire. C'est pourquoistd::function
le constructeur de peut prendre un allocateur.std::function
stocke une copie de son contenu. Comme pratiquement tous les types de bibliothèques standard C ++,function
utilise la sémantique des valeurs . Ainsi, il est copiable; lorsqu'il est copié, le nouvelfunction
objet 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».
la source
std::function
est 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 astd::function
, oui?std::function
objet sans mémoire dynamique allocation en cours.function
a 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.C ++ lambda est juste un sucre syntaxique autour de la classe Functor (anonyme) avec surchargé
operator()
etstd::function
est 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:
il fait cette sortie:
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)
la source