В чем магия оптимизации возвращаемой стоимости?

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

Однако создание объекта D "d4" следующим образом не вызывает ни одного из них:

D d4( foo() );  // foo returns a D

Вот код, который воспроизводит проблему, о которой я думал:

#include <iostream>
#include <vector>
#include <cassert>
using std::cout;
using std::endl;
    class D {
public:
    D()
    { cout << "D default"<<endl;}

    D(const D& d)
    {
        cout << "D copy" << endl;
    }

    D(D&& d)
    {
        cout << "D rval" << endl;
        assert(0);
    }

    D& operator=(D&& d)
    {
        cout << "D mv assign" << endl;
        return *this;
    }

    D& operator=(const D& d)
    {
        cout << "D copy assign" << endl;
        return *this;
    }

    volatile int v;
};

// return 
D foo()
{
    D res;
    cout <<"returning a non const D" << endl;
    return res;
}

int main()
{
    D d4(foo());

    return 0;
}

По сути, я предположил, что D(D&& d) будет вызываться для создания d4, поскольку foo() возвращает временное значение, адрес которого не может быть взят. На самом деле это было верно только тогда, когда оптимизация возвращаемого значения была отключена с помощью -fno-elide-constructors.

Однако, если он не был указан, оптимизация RV по умолчанию включена даже при -O0. Затем то, что я увидел, выглядит следующим образом:

D default
returning a non const D

Все, что я видел из стандартного вывода, пришло из foo(). Само создание d4 мне ничего не дало. Это отличается от того, что я ожидал.

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

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

Это ожидается? Если да, то в чем причина? Я тестировал как g++-7.3.0, так и clang++-5.0.1. Они были такими же.


person Stephen    schedule 06.04.2018    source источник
comment
Я думаю это (stackoverflow.com/questions/19792135/) является дубликатом. Предположим, я правильно понял вопрос.   -  person StoryTeller - Unslander Monica    schedule 06.04.2018
comment
В основном это: en.cppreference.com/w/cpp/language/copy_elision   -  person Rinat Veliakhmedov    schedule 06.04.2018
comment
В результате оптимизация возвращаемого значения повлияла на производительность программы. Верно. Вы найдете в документации, указанной в предыдущем комментарии, что исключение копирования может изменить наблюдаемые побочные эффекты. До C++ 14 это была единственная оптимизация, позволяющая делать такие вещи (поскольку тогда только выделение/расширение распределения также разрешено), поэтому не нужно бояться оптимизации C++, нарушающей ваши побочные эффекты за пределами этого очень конкретного.   -  person Max Langhof    schedule 06.04.2018


Ответы (2)


Поскольку вы используете C++17, который обещает RVO, даже если вы добавили -O0. это может помочь

person John Ding    schedule 06.04.2018
comment
Вы меня не обидели, просто резко отреагировали на какое-то воображаемое пренебрежение. - person StoryTeller - Unslander Monica; 06.04.2018

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

Хорошо, пока хорошо.

После этого в стеке вызывающего объекта выделяется еще одно пространство памяти. Поскольку возвращаемое значение является значением r, конструктор перемещения вызывается для записи чего-либо в «другое пространство памяти в стеке вызывающего объекта».

Ах, но здесь вы предположили неправильно. Видите ли, РВО — не единственный вид копирования. Копирование инициализации локальной переменной из временного возвращаемого значения также можно исключить, что и было сделано. Таким образом, нет «другого пространства памяти в стеке вызывающей стороны», поскольку объект был построен непосредственно в ячейке памяти переменной.

Это ожидается?

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

person eerorika    schedule 06.04.2018