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

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

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

Поэтому я хотел бы лучше понять, когда использовать или не использовать C ++ лямбды.

В настоящее время я понимаю, что лямбда без захваченного замыкания в точности похожа на обратный вызов Си. Однако, когда среда захватывается либо по значению, либо по ссылке, в стеке создается анонимный объект. Когда закрытие значения должно быть возвращено из функции, он помещается в std::function. Что происходит с закрытием памяти в этом случае? Копируется из стека в кучу? Освобождается ли он всякий раз, когда освобождается std::function, т. Е. Подсчитывается ли он как 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; }
}

В последнем случае замыкание строится на каждой итерации цикла, но, в отличие от предыдущего примера, оно дешевое, потому что оно подобно вызову функции, поэтому выделение кучи не производится. Кроме того, мне интересно, может ли компилятор «поднять» замыкание и сделать встраиваемые оптимизации.

Это правильно? Спасибо.

84 голоса | спросил Steve 30 PM00000090000001131 2012, 21:50:11

2 ответа


0
  

В настоящее время я понимаю, что лямбда без захваченного замыкания в точности похожа на обратный вызов C Однако, когда среда захватывается либо по значению, либо по ссылке, в стеке создается анонимный объект.

Нет; это всегда объект 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);

Где именно находится память для содержимого std::function зависит от реализации, но стирание типа используется std::function обычно требуется как минимум одно выделение памяти. Вот почему конструктор std::function может использовать распределитель.

  

Освобождается ли он каждый раз, когда освобождается функция std ::, т.е. подсчитывается ли он как std :: shared_ptr?

std::function хранит копию своего содержимого. Как и практически во всех типах стандартных библиотек C ++, function использует семантику значений . Таким образом, это копируемое; когда он копируется, новый объект function полностью отделяется. Он также может быть перемещен, поэтому любые внутренние выделения могут быть перенесены соответствующим образом без необходимости дополнительного выделения и копирования.

Таким образом, нет необходимости в подсчете ссылок.

Все остальное, что вы укажете, верно, если предположить, что «выделение памяти» приравнивается к «плохому использованию в коде реального времени».

ответил Nicol Bolas 30 PM000000100000001931 2012, 22:43:19
0

C ++ лямбда - это просто синтаксический сахар вокруг (анонимного) класса Functor с перегруженными operator() и std::function - это просто обертка вокруг вызываемых объектов (т. Е. Функторов, лямбд, c-функций, ...), которая копирует по значению «сплошной лямбда-объект msgstr "из текущей области стека - в кучу .

Чтобы проверить количество фактических конструкторов /перемещений, я сделал тест (используя другой уровень переноса в 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 (+ выделение кучи), чтобы создать его в std :: function, и другой для создания выделения кучи shared_ptr + конструирование функции)

ответил barney 3 MonEurope/Moscow2018-12-03T14:20:13+03:00Europe/Moscow12bEurope/MoscowMon, 03 Dec 2018 14:20:13 +0300 2018, 14:20:13

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132