Подтвердить что ты не робот

Компилируют ли компиляторы С++ 11 локальные переменные в значения r, когда они могут при оптимизации кода?

Иногда бывает разумно разделить сложные или длинные выражения на несколько шагов, например (вторая версия не более ясна, но это всего лишь пример):

return object1(object2(object3(x)));

может быть записана как:

object3 a(x);
object2 b(a);
object1 c(b);
return c;

Предполагая, что все 3 класса реализуют конструкторы, которые принимают значение rvalue в качестве параметра, первая версия может быть быстрее, поскольку временные объекты передаются и могут перемещаться. Я предполагаю, что во второй версии локальные переменные считаются lvalues. Но если переменные не используются позже, компиляторы С++ 11 оптимизируют код, поэтому переменные считаются значениями r, а обе версии работают одинаково? Меня больше всего интересует компилятор Visual Studio 2013 С++, но я также доволен тем, как компилятор GCC ведет себя в этом вопросе.

Спасибо, Михал

4b9b3361

Ответ 1

В этом случае компилятор не может нарушить правило "как есть". Но вы можете использовать std::move для достижения желаемого эффекта:

object3 a(x);
object2 b(std::move(a));
object1 c(std::move(b));
return c;

Ответ 2

Как сказал juanchopanza, компилятор не может (на уровне С++) нарушать правило "как-если"; то есть все преобразования должны генерировать семантически эквивалентный код.

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

Таким образом, это действительно зависит от самих объектов: если у движений-конструкторов/деструкторов есть побочные эффекты, а (де) выделение памяти является побочным эффектом, то оптимизация не может произойти. Если вы используете только POD, с конструкторами-конструкторами/деструкторами по умолчанию, то он, вероятно, будет автоматически оптимизирован.

Ответ 3

Но если переменные не используются позже, сделайте компиляторы С++ 11 оптимизированными код, поэтому переменные считаются значениями r и версии работают точно так же?

Возможно, но это сильно зависит от ваших типов. Рассмотрим следующий пример с типом POD point:

#include <cstdio>

struct point {
  int x;
  int y;
};

static point translate(point p, int dx, int dy) {
  return { p.x + dx, p.y + dy };
}

static point mirror(point p) {
  return { -p.x, -p.y };
}

static point make_point(int x, int y) {
  return { x, y };
}

int main() {
  point a = make_point(1, 2);
  point b = translate(a, 3, 3);
  point c = mirror(b);

  std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}

Я посмотрел на код сборки, вот в чем была скомпилирована вся программа (!) (поэтому приведенный ниже код является приближением С генерируемого кода сборки):

int main() {
  std::printf("(x,y) = (-4,-5)\n");
}

Он не только избавился от всех локальных переменных, но и выполнил вычисления во время компиляции! Я пробовал как gcc, так и clang, но не msvc.

ОК, так что сделайте программу немного более сложной, чтобы она не могла выполнять вычисления:

int main(int argc, char* argv[]) {

  int x = *argv[1]-'0';
  int y = *argv[2]-'0';
  point a = make_point(x,y);
  point b = translate(a, 3, 3);
  point c = mirror(b);

  std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}

Чтобы запустить этот код, вы должны называть его как ./a.out 1 2.

Вся эта программа сводится к этой (сборка, переписанная на C) после оптимизации:

int main(int argc, char* argv[]) {
  int x = *argv[1]-'0';
  int y = *argv[2]-'0';
  std::printf("(x,y) = (%d,%d)\n", -(x+3), -(y+3));
}

Таким образом, он избавился от a, b, c и всех функций make_point(), translate() и mirror() и сделал как можно больше вычислений во время компиляции.

По причинам, указанным в Matthieu M. answer, не ожидайте такой хорошей оптимизации с более сложными типами (особенно не-POD).

По моему опыту, inlining имеет решающее значение. Трудно работать, чтобы ваши функции были легко встроены. Используйте оптимизацию времени ссылки.

Ответ 4

Помните, что помимо семантики перемещения, которая может значительно ускорить ваш код, компилятор также выполняет (N) RVO - (Именованно) Оптимизацию возвращаемого значения, что может фактически повысить эффективность вашего кода. Я протестировал ваш пример и в g++ 4.8 кажется, что ваш второй пример может быть фактически более оптимальным:

object3 a(x);
object2 b(a);
object1 c(b);
return c;

Из моих экспериментов похоже, что он вызовет конструктор/деструктор 8 раз (1 ctr + 2 copy ctrs + 1 move ctr + 4 dtrs) по сравнению с другим методом, который вызывает его 10 раз (1 ctr + 4 move ctors + 5 dtors). Но по мере того как user2079303 прокомментировал, перемещение конструкторов должно по-прежнему превосходить конструкторы копирования, также в этом примере все вызовы будут встраиваться, поэтому служебные служебные вызовы функций не будут иметь места.

Копирование/перемещение elision на самом деле является исключением из правила "as-if", это означает, что иногда вы можете быть удивлены тем, что ваш конструктор/деструктор даже с побочными эффектами не вызван.

http://coliru.stacked-crooked.com/a/1ca7ebec0567e48f

(вы можете отключить (N) RVO с параметром -fno-elide-constructors)

#include <iostream>
#include <memory>

template<int S>
struct A {
    A() { std::cout<<"A::A"<<std::endl; }    
    template<int S2>
    A(const A<S2>&) { std::cout<<"A::A&"<<std::endl; }
    template<int S2>
    A(const A<S2>&&) { std::cout<<"A::A&&"<<std::endl; }    
    ~A() { std::cout<<"~A::A"<<std::endl;}        
};
A<0> foo () {    
    A<2> a; A<1> b(a); A<0> c(b); return c;   // calls dtor/ctor 8 times
    //return A<0>(A<1>(A<2>()));  // calls dtor/ctor 10 times
}
int main()
{
   A<0> a=foo();
   return 0;
}