Какая оптимизация влияет на семантику, если у нас уже есть RVO?

Насколько я понимаю, одной из целей добавления семантики перемещения является оптимизация кода путем вызова специального конструктора для копирования "временных" объектов. Например, в ответе this мы видим, что его можно использовать для оптимизации такого материала 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;
}

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

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

Ответы

Ответ 1

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

Да, функция подкачки:

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

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

Ответ 2

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

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

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

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

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

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

Ответ 3

Представьте, что ваш материал был классом с выделенной памятью кучи, подобной строке, и что у него было понятие емкости. Дайте ему оператор + =, который будет увеличивать емкость геометрически. В С++ 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 освобождения. Это переводит на более быстрый код.

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

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 никогда не используется. Затем вся программа удаляется, что приводит к пустой программе.

Ответ 8

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

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 по ссылке const, потому что алгоритм их подстраивает. Теоретически вы можете передать их по ссылке, но это выглядело бы грубо и оставило бы 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