Подтвердить что ты не робот

Как я могу сделать хранение лямбда-объектов С++ более эффективным?

Я думал о хранении lambda С++ в последнее время. Стандартный совет, который вы видите в Интернете, - это хранить лямбда в объекте std:: function. Однако ни один из этих советов никогда не учитывает последствия для хранилища. Мне пришло в голову, что за сценой должно быть какое-то серьезное черное вуду, чтобы сделать эту работу. Рассмотрим следующий класс, который хранит целочисленное значение:

class Simple {
public:
    Simple( int value ) { puts( "Constructing simple!" ); this->value = value; }
    Simple( const Simple& rhs ) { puts( "Copying simple!" ); this->value = rhs.value; }
    Simple( Simple&& rhs ) { puts( "Moving simple!" ); this->value = rhs.value; }
    ~Simple() { puts( "Destroying simple!" ); }
    int Get() const { return this->value; }

private:
    int value;
};

Теперь рассмотрим эту простую программу:

int main()
{
    Simple test( 5 );

    std::function<int ()> f =
        [test] ()
        {
            return test.Get();
        };

    printf( "%d\n", f() );
}

Это результат, который я надеюсь увидеть из этой программы:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

Сначала мы создаем тест значения. Мы создаем локальную копию в стеке для временного лямбда-объекта. Затем мы перемещаем временный лямбда-объект в память, выделенную std:: function. Мы уничтожаем временную лямбду. Мы печатаем нашу продукцию. Мы уничтожаем std:: function. И, наконец, мы уничтожаем тестовый объект.

Излишне говорить, что это не то, что я вижу. Когда я компилирую это на Visual С++ 2010 (режим выпуска или отладки), я получаю этот вывод:

Constructing simple!
Copying simple!
Copying simple!
Copying simple!
Copying simple!
Destroying simple!
Destroying simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

Святое дерьмо, что неэффективно! Мало того, что компилятор не смог использовать мой конструктор перемещения, но он сгенерировал и уничтожил две, по-видимому, лишние копии лямбда во время назначения.

Итак, здесь, наконец, есть вопросы: (1) Действительно ли все это копирование действительно необходимо? (2) Есть ли способ принудить компилятор к созданию лучшего кода? Спасибо за чтение!

4b9b3361

Ответ 1

Стандартные рекомендации, которые вы видите в Интернете, - это хранить лямбда в объекте std:: function. Тем не менее, ни один из этих советов никогда не учитывает последствия для хранилища.

Это потому, что это не имеет значения. У вас нет доступа к названию лямбда. Поэтому, хотя вы можете сначала сохранить его в своем родном типе с auto, он не оставляет эту область с этим типом. Вы не можете вернуть его как этот тип. Вы можете только придерживаться этого в чем-то другом. И единственным "чем-то другим" С++ 11 является std::function.

Итак, у вас есть выбор: временно удерживайте его с помощью auto, заблокированного в этой области. Или вставьте его в std::function для долговременного хранения.

Действительно ли все это копирование необходимо?

Технически? Нет, нет необходимости в том, что делает std::function.

Есть ли способ принудить компилятор к созданию лучшего кода?

Нет. Это не ошибка вашего компилятора; это то, как работает эта конкретная реализация std::function. Это может сделать меньше копирования; ему не нужно копировать более двух раз (и в зависимости от того, как компилятор генерирует лямбда, возможно, только один раз). Но это так.

Ответ 2

Я заметил ту же проблему с производительностью некоторое время назад с MSVC10 и подал отчет об ошибке в microsoft connect:
https://connect.microsoft.com/VisualStudio/feedback/details/649268/std-bind-and-std-function-generate-a-crazy-number-of-copy#details

Ошибка закрывается как "фиксированная". С предварительным просмотром разработчика MSVC11 ваш код теперь действительно печатает:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

Ответ 3

Ваша первая проблема заключается в том, что реализация MSVC std::function неэффективна. С g++ 4.5.1 я получаю:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

Это все еще создает дополнительную копию. Проблема в том, что ваша лямбда захватывает test по значению, поэтому у вас есть все копии. Попробуйте:

int main()
{
    Simple test( 5 );

    std::function<int ()> f =
        [&test] ()               // <-- Note added &
        {
            return test.Get();
        };

    printf( "%d\n", f() );
}

Снова с g++ теперь я получаю:

Constructing simple!
5
Destroying simple!

Обратите внимание, что если вы захватываете по ссылке, вы должны убедиться, что test остается в живых в течение срока службы f, иначе вы будете использовать ссылку на уничтоженный объект, который вызывает поведение undefined. Если f нужно пережить test, тогда вы должны использовать версию pass by value.

Ответ 4

Используя С++ 14, вы можете вообще избежать копий:

int main()
{
    Simple test( 5 );

    std::function<int ()> f =
    [test = std::move(test)] ()
    {
        return test.Get();
    };

    printf( "%d\n", f() );
}

Для вывода вывода:

Constructing simple!
Moving simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

Обратите внимание на следующую строку:

[test = std::move(test)] 

Здесь первое появление "теста" отличается от второго.

Ответ 5

Проблема заключается в том, что std:: function не использует семантику перемещения и копирует лямбда вокруг при инициализации. Это плохая реализация MS.

Вот небольшой трюк, который вы можете использовать, чтобы обойти эту проблему.

template<typename T>
class move_lambda
{
    T func_;

public:
    move_lambda(T&& func) : func_(std::move(func)){}            
    move_lambda(const move_lambda& other) : func_(std::move(other.func_)){} // move on copy 
    auto operator()() -> decltype(static_cast<T>(0)()){return func_();}
};

template <typename T>
move_lambda<T> make_move_lambda(T&& func)
{
    return move_lambda<T>(std::move(func));
}

использование:

int main()
{
    Simple test( 5 );

    std::function<int ()> f(make_move_lambda(
        [test] ()
        {
            return test.Get();
        }));

    printf( "%d\n", f() );
}