Как я могу сделать хранение лямбда-объектов C ++ более эффективным? - PullRequest
21 голосов
/ 20 ноября 2011

В последнее время я думаю о хранении лямбды C ++. Стандартный совет, который вы видите в Интернете, - хранить лямбду в объекте 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 ::. И, наконец, мы уничтожаем тестовый объект.

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

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

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

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

Ответы [ 5 ]

10 голосов
/ 20 ноября 2011

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

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

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

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

Технически? Нет, не нужно для того, что std::function делает.

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

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

8 голосов
/ 21 ноября 2011

Я недавно заметил ту же проблему с производительностью 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!
6 голосов
/ 20 ноября 2011

Ваша первая проблема заключается в том, что реализация 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, в противном случае вы будете использовать ссылку на уничтоженный объект, что вызывает неопределенное поведение.Если f нужно пережить test, вам нужно использовать версию передачи по значению.

3 голосов
/ 03 мая 2014

Используя C ++ 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)] 

Здесь первое появление «теста» находится в другой области видимости, чем второе.

0 голосов
/ 20 ноября 2011

Проблема в том, что 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() );
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...