Ламбда-реалізація C ++ 11 та модель пам'яті


92

Я хотів би отримати деяку інформацію про те, як правильно думати про закриття C ++ 11, а також std::functionпро те, як вони реалізовані та як обробляється пам’ять.

Незважаючи на те, що я не вірю в передчасну оптимізацію, я маю звичку ретельно розглядати вплив мого вибору на продуктивність під час написання нового коду. Я також роблю велику кількість програм у реальному часі, наприклад, на мікроконтролерах та аудіосистемах, де слід уникати недетермінованих пауз розподілу / вивільнення пам'яті.

Тому я хотів би краще зрозуміти, коли використовувати чи не використовувати лямбди С ++.

На моєму теперішньому розумінні, лямбда без захопленого замикання точно відповідає зворотному виклику C. Однак, коли середовище фіксується або за значенням, або за посиланням, у стеку створюється анонімний об'єкт. Коли закриття значення повинно бути повернуто з функції, воно обертає його std::function. Що в цьому випадку відбувається із закритою пам’яттю? Це скопійовано зі стеку в купу? Чи звільняється воно щоразу, коли std::functionзвільняється, тобто чи вважається воно посиланням як a std::shared_ptr?

Я уявляю, що в системі реального часу я міг би створити ланцюжок лямбда-функцій, передаючи B як аргумент продовження A, так що A->Bбуде створений конвеєр обробки . У цьому випадку закриття A та B буде розподілено один раз. Хоча я не впевнений, чи вони будуть виділені у стек чи купу. Однак загалом це видається безпечним для використання в системі реального часу. З іншого боку, якщо B створює якусь лямбда-функцію C, яку вона повертає, тоді пам'ять для C буде виділятися і вивільнятися багаторазово, що не буде прийнятним для використання в режимі реального часу.

У псевдокоді цикл DSP, який, на мою думку, буде безпечним у реальному часі. Я хочу виконати обробку блоку A, а потім B, де A викликає свій аргумент. Обидві ці функції повертають std::functionоб'єкти, тому fбуде std::functionоб'єктом, де його оточення зберігається в купі:

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)
}

І той, який, на мою думку, може бути погано використовувати в коді в реальному часі:

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

І той, де, на мою думку, для закриття використовується пам’ять стека:

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

В останньому випадку закриття будується на кожній ітерації циклу, але на відміну від попереднього прикладу воно дешеве, оскільки воно подібне до виклику функції, розподіл купи не проводиться. Більше того, мені цікаво, чи міг компілятор "зняти" закриття та зробити вбудовані оптимізації.

Це правильно? Дякую.


4
При використанні лямбда-виразу немає накладних витрат. Іншим вибором було б написати такий об’єкт функції самостійно, що б точно так само. До речі, щодо вбудованого питання, оскільки компілятор має всю необхідну інформацію, він, безсумнівно, може просто вбудувати виклик до operator(). Немає жодного "підйому", лямбди не є чимось особливим. Вони просто короткий опис локального об’єкта функції.
Xeo

Здається, це питання про те, std::functionзберігає свій стан на купі чи ні, і не має нічого спільного з лямбдами. Це так?
Мукінг качка

8
Просто записати це в разі будь - яких непорозумінь: вираз Лямбда це НЕstd::function !!
Xeo

1
Просто побічний коментар: будьте обережні, повертаючи лямбда-функцію з функції, оскільки будь-які локальні змінні, захоплені посиланням, стають недійсними після виходу з функції, яка створила лямбда-функцію.
Джорджо

2
@ Steve, починаючи з C ++ 14, ви можете повернути лямбда-функцію з функції із autoтипом повернення.
Окталіст

Відповіді:


100

На моєму теперішньому розумінні, лямбда без зафіксованого закриття - це точно як зворотний виклик C. Однак, коли середовище фіксується або значенням, або посиланням, у стеку створюється анонімний об'єкт.

Немає; це завжди об’єкт С ++ невідомого типу, створений у стеку. Лямбда без захоплення може бути перетворена на покажчик функції (хоча чи підходить він для конвенцій викликів C, залежить від реалізації), але це не означає, що це покажчик функції.

Коли закриття значення повинно бути повернуто з функції, воно обертається в std :: function. Що в цьому випадку відбувається із закритою пам’яттю?

Лямбда не є чимось особливим у C ++ 11. Це об’єкт, як і будь-який інший об’єкт. Лямбда-вираз призводить до тимчасового, який можна використовувати для ініціалізації змінної в стеку:

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

lambє об'єктом стека. Він має конструктор і деструктор. І він буде дотримуватися всіх правил C ++ для цього. Тип lambбуде містити значення / посилання, які були зафіксовані; вони будуть членами цього об'єкта, як і будь-які інші об'єкти будь-якого іншого типу.

Ви можете передати його std::function:

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

У цьому випадку він отримає копію значення lamb. Якби lambзахопив щось за значенням, було б дві копії цих значень; один в lamb, і один в func_lamb.

Коли поточна область дії закінчиться, func_lambвона буде знищена, за якою слідуватиме lamb, згідно з правилами очищення змінних стека.

Ви можете так само легко виділити один з купи:

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

Саме те, куди пам'ять для вмісту a, std::functionзалежить від реалізації, але стирання типу, яке використовується, std::functionяк правило, вимагає принаймні одного розподілу пам'яті. Ось чому std::functionконструктор може взяти розподільник.

Чи звільняється він щоразу, коли звільняється функція std ::, тобто чи вважається вона посиланням як std :: shared_ptr?

std::functionзберігає копію його вмісту. Як і практично кожен стандартний тип бібліотеки C ++, functionвикористовує семантику значень . Таким чином, його можна скопіювати; при його копіюванні новий functionоб'єкт є абсолютно окремим. Він також є рухомим, тому будь-які внутрішні розподіли можуть бути передані належним чином, не потребуючи більше розподілу та копіювання.

Таким чином, немає необхідності в підрахунку посилань.

Все інше, що ви заявляєте, є правильним, припускаючи, що "виділення пам'яті" дорівнює "поганому для використання в коді в реальному часі".


1
Відмінне пояснення, дякую. Отже, створення std::function- це точка, в якій пам’ять виділяється та копіюється. Здається, з цього випливає, що немає можливості повернути закриття (оскільки вони розміщені в стеку), без попереднього копіювання в a std::function, так?
Стів

3
@ Steve: Так; вам потрібно обернути лямбду в якийсь контейнер, щоб він вийшов із зони дії.
Нікол Болас

Чи скопійований весь код функції, чи оригінальна функція призначена під час компіляції та передала закриті значення?
Llamageddon

Я хочу додати, що стандарт більш-менш опосередковано вимагає (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5), що якщо лямбда нічого не захоплює, вона може зберігатися в std::functionоб'єкті без динамічної пам'яті розподіл триває.
5gon12eder

2
@Yakk: Як ви визначаєте "великий"? Об'єкт із двома покажчиками стану "великий"? Як щодо 3 чи 4? Крім того, розмір об’єкта - не єдине питання; якщо об'єкт не можна перемістити, його потрібно зберігати у розподілі, оскільки він functionмає конструктор переміщення noexcept. Весь сенс сказати "загалом вимагає" полягає в тому, що я не кажу " завжди вимагає": існують обставини, коли розподіл проводитись не буде.
Nicol Bolas,

0

С ++ лямбда - це просто синтаксичний цукор навколо (анонімного) класу функторів із перевантаженням operator()і std::functionпросто обгортка навколо викликів (тобто функторів, лямбда, c-функцій, ...), який копіює за значенням "твердий лямбда-об'єкт" із поточного область стека - до купи .

Щоб перевірити кількість фактичних конструкторів / релокатонів, я зробив тест (використовуючи інший рівень обтікання до shared_ptr, але це не так). Переконайтесь самі:

#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)();
}

це робить цей результат:

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

Точно такий же набір ctors / dtors буде викликатися для виділеного стеком лямбда-об'єкта! (Тепер він викликає Ctor для розподілу стека, Copy-ctor (+ heap alloc), щоб побудувати його в std :: function та ще один для здійснення розподілу кучі shared_ptr + побудова функції)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.