Можно ли оптимизировать создание составных объектов из временных? - PullRequest
4 голосов
/ 03 мая 2011

Я задал несколько вопросов, которые касались этой проблемы, но я получил разные ответы, поэтому я подумал, что лучше задать их напрямую.

Допустим, у нас есть следующий код:

// Silly examples of A and B, don't take so seriously, 
// just keep in mind they're big and not dynamically allocated.
struct A { int x[1000]; A() { for (int i = 0; i != 1000; ++i) { x[i] = i * 2; } };
struct B { int y[1000]; B() { for (int i = 0; i != 1000; ++i) { y[i] = i * 3; } };

struct C
{
  A a;
  B b;
};

A create_a() { return A(); }
B create_b() { return B(); }

C create_c(A&& a, B&& b)
{
  C c;
  c.a = std::move(a);
  c.b = std::move(b);
  return C; 
};

int main()
{
  C x = create_c(create_a(), create_b());
}

В идеале create_c(A&&, B&&) должно быть неактивным. Вместо того, чтобы соглашение о вызовах предусматривало создание A и B и передачу ссылок на них в стеке, A и B должны создаваться и передаваться по значению вместо возвращаемого значения, c. С NRVO это будет означать создание и передачу их непосредственно в x, без дальнейшей работы для функции create_c.

Это позволит избежать необходимости создавать копии A и B.

Есть ли какой-нибудь способ разрешить / поощрить / принудить это поведение от компилятора, или оптимизирующие компиляторы вообще так или иначе делают это? И это будет работать только тогда, когда компилятор встроит функции, или будет работать на единицах компиляции.

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

Если create_a() и create_b() принимают скрытый параметр того, куда поместить возвращаемое значение, они могут поместить результаты непосредственно в x, который затем передается по ссылке на create_c(), которая ничего не должна делать и немедленно возвращается.

Ответы [ 3 ]

4 голосов
/ 03 мая 2011

Существуют разные способы оптимизации кода, который есть у вас, но ссылки на значения не совпадают.Проблема в том, что ни A, ни B не могут быть перемещены бесплатно, поскольку вы не можете украсть содержимое объекта.Рассмотрим следующий пример:

template <typename T>
class simple_vector {
   typedef T element_type;
   typedef element_type* pointer_type;
   pointer_type first, last, end_storage;
public:
   simple_vector() : first(), last(), end_storage() {}
   simple_vector( simple_vector const & rhs )              // not production ready, memory can leak from here!
      : first( new element_type[ rhs.last - rhs.first ] ),
        last( first + rhs.last-rhs.first ),
        end_storage( last )
   {
       std::copy( rhs.first, rhs.last, first );
   }
   simple_vector( simple_vector && rhs ) // we can move!
      : first( rhs.first ), last( rhs.last ), end_storage( rhs.end_storage )
   {
      rhs.first = rhs.last = rhs.end_storage = 0;
   }
   ~simple_vector() {
      delete [] rhs.first;
   }
   // rest of operations
};

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

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

Конечно, поскольку вы используете выделенные в стеке объекты в коде, старый (N) RVO может использоваться компилятором, а когда вы делаете: C c = { create_a(), create_b() }; компилятор может выполнить эту оптимизацию(в основном установите атрибут c.a для адреса возвращаемого объекта из create_a, в то время как при компиляции create_a создайте возвращенный временный объект непосредственно oТаким же адресом, так эффективно, c.a, возвращенный объект из create_a и временный объект, созданный внутри create_a (неявный this для конструктора) являются одним и тем же объектом, исключая две копии,То же самое можно сделать с c.b, избегая затрат на копирование.Если компилятор встроит ваш код, он удалит create_c и заменит его конструкцией, аналогичной: C c = {create_a(), create_b()};, так что он может потенциально оптимизировать удаление всех копий.

С другой стороны, обратите внимание, что эта оптимизацияне может быть полностью использован в случае объекта C, выделенного динамически, как в C* p = new C; p->a = create_a();, так как назначение , а не в стеке, компилятор может только оптимизировать временное внутреннее create_a и его возвратзначение, но оно не может совпадать с p->a, поэтому необходимо сделать копию.В этом преимущество rvalue-reference перед (N) RVO, но, как упоминалось ранее, вы не можете эффективно использовать rvalue-reference в своем примере кода напрямую.

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

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

  1. Функция Inlining (В случае A, B и C (и A и B, которые он содержит))
  2. Копировать elision (только C (и A и B, содержащиеся в нем), поскольку вы вернули C по значению)

Для такой маленькой функции она, вероятно, будет встроенной. Практически любой компилятор сделает это, если он существует в одном и том же модуле перевода, а хорошие компиляторы, такие как MSVC ++ и G ++ (и я думаю, что LLVM, но я не уверен в этом), имеют настройки оптимизации всей программы, которые сделают это даже во всех переводческие единицы. Если функция встроенная, то да, вызов функции (и копия, которая идет с ней) вообще не будет происходить.

Если по какой-то причине функция не становится встроенной (т. Е. Вы использовали __declspec(noinline) в MSVC ++), то вы все равно будете иметь право на Именованная оптимизация возвращаемого значения ( NRO) , которые все хорошие компиляторы C ++ (опять же, MSVC ++, G ++ и, я думаю, LLVM) все реализуют. По сути, стандарт гласит, что компиляторам разрешено не выполнять копирование при возврате, если они могут этого избежать, и они обычно генерируют код, который его избегает. Есть несколько вещей, которые вы можете сделать, чтобы отключить NRVO, но по большей части это довольно безопасная оптимизация, на которую можно положиться.

Наконец, профиль. Если вы видите проблему с производительностью, то выясните что-то еще. В противном случае я написал бы вещи идеологическим способом и заменил бы их более производительными конструкциями, если и только если вам нужно.

0 голосов
/ 03 мая 2011

Не очевидно, что нужно дать C конструктор и затем сказать:

C create_c(const A & a, const B & b)
{
  return C( a, b );
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...