Какую оптимизацию обеспечивает семантика перемещения, если у нас уже есть RVO? - PullRequest
31 голосов
/ 17 февраля 2011

Насколько я понимаю, одной из целей добавления семантики перемещения является оптимизация кода путем вызова специального конструктора для копирования "временных" объектов.Например, в этом ответе мы видим, что его можно использовать для оптимизации таких string a = x + y вещей.Поскольку x + y является выражением rvalue, вместо глубокого копирования мы можем скопировать только указатель на строку и размер строки.Но, как мы знаем, современные компиляторы поддерживают оптимизацию возвращаемого значения , поэтому без использования семантики перемещения наш код вообще не будет вызывать конструктор копирования.

Чтобы доказать это, я пишу этот код:

#include <iostream>

struct stuff
{
        int x;
        stuff(int x_):x(x_){}
        stuff(const stuff & g):x(g.x)
        {
                std::cout<<"copy"<<std::endl;
        }
};   
stuff operator+(const stuff& lhs,const stuff& rhs)
{
        stuff g(lhs.x+rhs.x);
        return g;
}
int main()
{
        stuff a(5),b(7);
        stuff c = a+b;
}

И после его выполнения в VC ++ 2010 и g ++ в режиме оптимизации я получаю пустой вывод.

Что это за оптимизация, если без нее мой код все еще работает быстрее?Не могли бы вы объяснить, что я неправильно понимаю?

Ответы [ 8 ]

24 голосов
/ 17 февраля 2011

Семантика перемещения не должна рассматриваться как устройство оптимизации, даже если они могут использоваться как таковые.

Если вы хотите получить копии объектов (либо параметров функции, либо возвращаемых значений), тогда RVO и elision copy выполнят эту работу, когда смогут. Семантика перемещения может помочь, но она более мощная.

Семантика перемещения удобна, когда вы хотите сделать что-то другое , является ли переданный объект временным (затем он связывается с ссылкой на значение ) или «стандартным» объектом с имя (так называемое const lvalue ). Если вы хотите, например, украсть ресурсы временного объекта, то вам нужна семантика перемещения (например: вы можете украсть содержимое, на которое указывает std::unique_ptr).

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

Не копируемые объекты хороши, так как они не заставляют вас создавать подверженный ошибкам конструктор копирования. В большинстве случаев семантика копирования на самом деле не имеет смысла, но семантика перемещения делает (подумайте об этом).

Это также позволяет вам использовать подвижные std::vector<T> классы, даже если T не копируется. Шаблон класса std::unique_ptr также является отличным инструментом при работе с не копируемыми объектами (например, полиморфными объектами).

9 голосов
/ 17 февраля 2011

После некоторого копания я нахожу этот превосходный пример оптимизации со ссылками на rvalue в FAQ Страуструпа .

Да, функция обмена:

    template<class T> 
void swap(T& a, T& b)   // "perfect swap" (almost)
{
    T tmp = move(a);    // could invalidate a
    a = move(b);        // could invalidate b
    b = move(tmp);      // could invalidate tmp
}

Это сгенерирует оптимизированный код для любых типов типов (при условии, что у него есть конструктор перемещения).

Редактировать: Также RVO не может оптимизировать что-то вроде этого (по крайней мере, на моем компиляторе):

stuff func(const stuff& st)
{
    if(st.x>0)
    {
        stuff ret(2*st.x);
        return ret;
    }
    else
    {
        stuff ret2(-2*st.x);
        return ret2;
    }
}

Эта функция всегда вызывает конструктор копирования (проверяется с помощью VC ++). И если наш класс можно перемещать быстрее, чем с конструктором перемещения, у нас будет оптимизация.

7 голосов
/ 17 февраля 2011

Представьте, что ваш материал был классом с выделенной кучей памяти, как строка, и что он имел понятие емкости. Дайте ему оператор + =, который увеличит емкость геометрически. В C ++ 03 это может выглядеть так:

#include <iostream>
#include <algorithm>

struct stuff
{
    int size;
    int cap;

    stuff(int size_):size(size_)
    {
        cap = size;
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    stuff(const stuff & g):size(g.size), cap(g.cap)
    {
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    ~stuff()
    {
        if (cap > 0)
            std::cout << "deallocating " << cap << '\n';
    }

    stuff& operator+=(const stuff& y)
    {
        if (cap < size+y.size)
        {
            if (cap > 0)
                std::cout << "deallocating " << cap << '\n';
            cap = std::max(2*cap, size+y.size);
            std::cout <<"allocating " << cap <<std::endl;
        }
        size += y.size;
        return *this;
    }
};

stuff operator+(const stuff& lhs,const stuff& rhs)
{
    stuff g(lhs.size + rhs.size);
    return g;
}

Также представьте, что вы хотите добавить больше, чем две вещи одновременно:

int main()
{
    stuff a(11),b(9),c(7),d(5);
    std::cout << "start addition\n\n";
    stuff e = a+b+c+d;
    std::cout << "\nend addition\n";
}

Для меня это печатает:

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
allocating 27
allocating 32
deallocating 27
deallocating 20

end addition
deallocating 32
deallocating 5
deallocating 7
deallocating 9
deallocating 11

Я считаю 3 распределения и 2 освобождения для вычисления:

stuff e = a+b+c+d;

Теперь добавьте семантику перемещения:

    stuff(stuff&& g):size(g.size), cap(g.cap)
    {
        g.cap = 0;
        g.size = 0;
    }

...

stuff operator+(stuff&& lhs,const stuff& rhs)
{
        return std::move(lhs += rhs);
}

Запустив снова, я получаю:

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
deallocating 20
allocating 40

end addition
deallocating 40
deallocating 5
deallocating 7
deallocating 9
deallocating 11

Теперь у меня 2 распределения и 1 освобождение. Это означает более быстрый код.

3 голосов
/ 17 февраля 2011

Есть много мест, некоторые из которых упоминаются в других ответах.

Один большой из них заключается в том, что при изменении размера std::vector он будет перемещать объекты, учитывающие движение, из старой области памяти в новую, а не копировать и уничтожать оригинал.

Кроме того, ссылки на rvalue допускают концепцию подвижных типов, это семантическая разница, а не просто оптимизация. unique_ptr было невозможно в C ++ 03, поэтому у нас была мерзость auto_ptr.

1 голос
/ 17 февраля 2011

Ваш опубликованный пример принимает только ссылки const lvalue, и поэтому явно не может применять к нему семантику перемещения, так как там нет ни одной ссылки на rvalue. Как перемещение семантики может сделать ваш код быстрее, если вы реализовали тип без ссылок на rvalue?

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

1 голос
/ 17 февраля 2011

Тот факт, что данный конкретный случай уже охвачен существующей оптимизацией, не означает, что не существует других случаев, когда ссылки на r-значения полезны.

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

0 голосов
/ 14 ноября 2017

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

Matrix MyAlgorithm(Matrix U, Matrix V)
{
    Transform(U); //doesn't matter what this actually does, but it modifies U
    Transform(V);
    return U*V;
}

Обратите внимание, что вы не можете передать U и V по константной ссылке, потому что алгоритм их настраивает. Вы можете теоретически передать их по ссылке, но это будет выглядеть грубым и оставить U и V в некотором промежуточном состоянии (так как вы вызываете Transform(U)), что может не иметь никакого смысла для вызывающего или просто не иметь никакого значения математический смысл вообще, так как это всего лишь одно из внутренних преобразований алгоритма. Код выглядит намного чище, если вы просто передаете их по значению и используете семантику перемещения, если вы не собираетесь использовать U и V после вызова этой функции:

Matrix u, v;
...
Matrix w = MyAlgorithm(u, v); //slow, but will preserve u and v
Matrix w = MyAlgorithm(move(u), move(v)); //fast, but will nullify u and v
Matrix w = MyAlgorithm(u, move(v)); //and you can even do this if you need one but not the other
0 голосов
/ 08 сентября 2013

Эта строка вызывает первый конструктор.

stuff a(5),b(7);

Оператор Plus вызывается с использованием явных общих ссылок lvalue.

stuff c = a + b;

Внутри метода перегрузки оператора у вас нет вызванного конструктора копирования. Опять же, первый конструктор вызывается только.

stuff g(lhs.x+rhs.x);

присвоение производится с помощью RVO, поэтому копия не требуется. НЕ требуется копия из возвращенного объекта в 'c'.

stuff c = a+b;

Из-за отсутствия ссылки std::cout компилятор позаботится о том, чтобы значение c никогда не использовалось. Затем вся программа удаляется, в результате чего получается пустая программа.

...