Ошибка компиляции RVO при сбое

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

До сих пор мне частично удавалось выдавать ошибки ссылки на код при сбое RVO. Для этого я объявляю конструкторы копирования, не определяя их. Очевидно, что это не является ни надежным, ни осуществимым в тех редких случаях, когда мне нужно реализовать один или оба конструктора копирования, т.е. x(x&&) а также x(x const&),

Это подводит меня ко второму вопросу: Почему создатели компилятора выбрали включение RVO, когда существуют определенные пользователем конструкторы копирования, а не когда присутствуют только конструкторы копирования по умолчанию?

Третий вопрос: Есть ли другой способ включить RVO для простых структур данных?

Последний вопрос (обещание): Знаете ли вы какой-нибудь компилятор, который заставляет мой тестовый код вести себя иначе, чем я наблюдал с gcc и clang?

Вот пример кода для gcc 4.6, gcc 4.8 и clang 3.3, который показывает проблему. Поведение не зависит от общих настроек оптимизации или отладки. Конечно вариант --no-elide-constructors делает то, что говорит, то есть выключает RVO.

#include <iostream>
using namespace std;

struct x
{
x () { cout << "original x address" << this << endl; }
};
x make_x ()
{
return x();
}

struct y
{
y () { cout << "original y address" << this << endl; }
// Any of the next two constructors will enable RVO even if only
// declared but not defined. Default constructors will not do!
y(y const & rhs);
y(y && rhs);
};
y make_y ()
{
return y();
}

int main ()
{
auto x1 = make_x();
cout << "copy of  x address" << &x1 << endl;
auto y1 = make_y();
cout << "copy of  y address" << &y1 << endl;
}

Выход:

original x address0x7fff8ef01dff
copy of  x address0x7fff8ef01e2e
original y address0x7fff8ef01e2f
copy of  y address0x7fff8ef01e2f

RVO, похоже, также не работает с простыми структурами данных:

#include <iostream>

using namespace std;

struct x
{
int a;
};

x make_x ()
{
x tmp;
cout << "original x address" << &tmp << endl;
return tmp;
}

int main ()
{
auto x1 = make_x();
cout << "copy of  x address" << &x1 << endl;
}

Выход:

original x address0x7fffe7bb2320
copy of  x address0x7fffe7bb2350

ОБНОВИТЬ: Обратите внимание, что некоторые оптимизации очень легко спутать с RVO. Помощники конструктора, такие как make_x являются примером. Увидеть этот пример где оптимизация фактически обеспечивается стандартом.

9

Решение

Проблема в том, что компилятор делает слишком много оптимизаций 🙂

Прежде всего, я отключил встраивание make_x() в противном случае мы не можем различить RVO и встраивание. Однако я поместил остальное в анонимное пространство имен, чтобы внешняя связь не мешала другим оптимизациям компилятора. (Как показывают факты, внешняя связь может предотвратить, например, встраивание, и кто знает, что еще …) Я переписал ввод-вывод, теперь он использует printf(); в противном случае сгенерированный код сборки будет загроможден из-за всех iostream вещи. Итак, код:

#include <cstdio>
using namespace std;

namespace {

struct x {
//int dummy[1024];
x() { printf("original x address %p\n", this); }
};

__attribute__((noinline)) x make_x() {
return x();
}

} // namespace

int main() {
auto x1 = make_x();
printf("copy  of x address %p\n", &x1);
}

Я проанализировал сгенерированный код сборки с моим коллегой, так как мое понимание сгенерированной сборки gcc очень ограничено. Позже сегодня я использовал лязг с -S -emit-llvm флаги для генерации Сборка LLVM который я лично нахожу намного приятнее и легче читать, чем Сборка X86 / Синтаксис GAS. Не важно, какой компилятор использовался, выводы те же.

Я переписал сгенерированную сборку в C ++, это выглядит примерно так, если x пустой:

#include <cstdio>
using namespace std;

struct x { };

void make_x() {
x tmp;
printf("original x address %p\n", &tmp);
}

int main() {
x x1;
make_x();
printf("copy  of x address %p\n", &x1);
}

Если x большой ( int dummy[1024]; Участник без комментариев):

#include <cstdio>
using namespace std;

struct x { int dummy[1024]; };

void make_x(x* x1) {

printf("original x address %p\n", x1);
}

int main() {
x x1;
make_x(&x1);
printf("copy  of x address %p\n", &x1);
}

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

Если вы сделаете объект больше (добавьте int dummy[1024]; член), он создается на месте, поэтому RVO начинает работать, и только адрес объекта передается make_x() быть напечатанным. Ни один объект не копируется, ничего не перемещается.

Если объект пуст, компилятор может решить не передавать адрес make_x() (Что за пустая трата ресурсов?) make_x() создать уникальный, действительный адрес из своего собственного стека. Когда эта оптимизация происходит, это немного нечетко и трудно рассуждать (это то, что вы видите с y) но это действительно не имеет значения.

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

5

Другие решения

  1. Я не верю, что есть какой-то способ сделать такую ​​гарантию. РВО является оптимизация и как таковой, компилятор может определить в конкретном случае, что его использование фактически является де-оптимизацией, и принять решение не делать этого.

  2. Я предполагаю, что вы имеете в виду свой первый фрагмент кода. В 32-битной компиляции я не могу воспроизвести ваше утверждение на g ++ 4.4, 4.5 или 4.8 (через ideone.com) даже без оптимизации вообще. В 64-битной компиляции я могу воспроизвести ваше поведение без RVO. Это пахнет как ошибка генерации 64-битного кода в g ++.

  3. Если на самом деле то, что я заметил в (2) это ошибка, то, как только ошибка будет исправлена, она будет работать.

  4. Я могу подтвердить, что Sun CC также не RVO ваши конкретные примеры даже в 32-битной компиляции.

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

2

Почему создатели компилятора выбрали включение RVO, когда существуют определенные пользователем конструкторы копирования, а не когда присутствуют только конструкторы копирования по умолчанию?

Потому что стандарт говорит так:

С ++ 14, 12,8 / 31:

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

С ++ 14, 12,8 / 32

Когда критерии для исключения операции копирования выполнены или будут выполнены, за исключением того факта, что исходный объект является параметром функции, а копируемый объект обозначен lvalue, разрешением перегрузки для выбора конструктора для копирования является сначала выполняется так, как если бы объект был обозначен как rvalue. Если не удается разрешить перегрузку или если тип первого параметра выбранного конструктора не является rvalue-ссылкой на тип объекта (возможно, cv-квалифицированный), разрешение перегрузки выполняется снова, рассматривая объект как lvalue. [Примечание: это двухэтапное разрешение перегрузки должно выполняться независимо от того, будет ли выполнено копирование. Он определяет конструктор, который будет вызван, если elision не выполняется, и выбранный конструктор должен быть доступен, даже если вызов исключен. —Конечная записка]

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

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

Это означает, что код всегда должен быть действительным, даже если компилятор по какой-то причине решает НЕ выполнять оптимизацию RVO.

0