возврат по значению встроенных функций - PullRequest
7 голосов
/ 21 августа 2009

Я реализую некоторые математические типы и хочу оптимизировать операторы, чтобы минимизировать объем создаваемой, уничтожаемой и копируемой памяти. Для демонстрации я покажу вам часть моей реализации Quaternion.

class Quaternion
{
public:
    double w,x,y,z;

    ...

    Quaternion  operator+(const Quaternion &other) const;
}

Я хочу знать, как две следующие реализации отличаются друг от друга. У меня есть реализация + =, которая работает на месте, где не создается память, но некоторые операции более высокого уровня, использующие кватернионы, полезно использовать +, а не + =.

__forceinline Quaternion Quaternion::operator+( const Quaternion &other ) const
{
    return Quaternion(w+other.w,x+other.x,y+other.y,z+other.z);
}

и

__forceinline Quaternion Quaternion::operator+( const Quaternion &other ) const
{
    Quaternion q(w+other.w,x+other.x,y+other.y,z+other.z);
    return q;
}

Мой c ++ полностью самоучка, поэтому, когда дело доходит до некоторой оптимизации, я не уверен, что делать, потому что я не знаю точно, как компилятор обрабатывает эти вещи. Также, как эти механизмы переводятся в не встроенные реализации.

Любая другая критика моего кода приветствуется.

Ответы [ 4 ]

10 голосов
/ 22 августа 2009

Ваш первый пример позволяет компилятору потенциально использовать что-то, называемое «Оптимизация возвращаемого значения» (RVO).

Второй пример позволяет компилятору потенциально использовать то, что называется «Оптимизация именованного возвращаемого значения» (NRVO). Эти 2 оптимизации явно тесно связаны.

Некоторые подробности реализации Microsoft NRVO можно найти здесь:

Обратите внимание, что в статье указано, что поддержка NRVO началась с VS 2005 (MSVC 8.0). В нем конкретно не говорится, применимо ли это к RVO или нет, но я считаю, что MSVC использовала оптимизацию RVO до версии 8.0.

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

Включая этот бит:

Вы будете разочарованы, узнав, что каждый компилятор, а часто и каждая версия компилятора, имеют свои собственные правила для обнаружения и применения RVO. Некоторые применяют RVO только к функциям, возвращающим неназванные временные значения (самая простая форма RVO). Более сложные также применяют RVO, когда есть именованный результат, который возвращает функция (так называемый Named RVO, или NRVO).

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

Статья была написана в 2003 году, и компиляторы должны быть значительно улучшены; надеюсь, фаза луны менее важна, чем когда компилятор может использовать RVO / NRVO (может быть, это до дня недели). Как отмечалось выше, похоже, что MS не внедряла NRVO до 2005 года. Возможно, именно тогда кто-то, работающий над компилятором в Microsoft, получил новую пару более удобных ботинок, на половину больше, чем раньше.

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

6 голосов
/ 21 августа 2009

Между двумя представленными реализациями нет никакой разницы. Любой компилятор, выполняющий любые виды оптимизации, оптимизирует вашу локальную переменную.

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

2 голосов
/ 22 августа 2009

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

// thread safety is not a problem
class Q
{
   double w,x,y,z;
public:
   // constructors, other operators, other methods... omitted
   Q& operator+=( Q const & rhs ) {
      w += rhs.w;
      x += rhs.x;
      y += rhs.y;
      z += rhs.z;
      return *this;
   }
};
Q operator+( Q lhs, Q const & rhs ) {
   lhs += rhs;
   return lhs;
}

Это имеет следующие преимущества:

  • Только одна реализация логики. Если класс меняется, вам нужно только переопределить операторы? = И операторы? будет адаптироваться автоматически.
  • Оператор свободной функции симметричен относительно неявных преобразований компилятора
  • Это самая эффективная реализация оператора? Вы можете найти в отношении копий

Оперативность оператора?

Когда вы звоните оператору? на двух элементах третий объект должен быть создан и возвращен. Используя описанный выше подход, копия выполняется в вызове метода. Как таковой, компилятор может исключить копию, когда вы передаете временный объект. Обратите внимание, что это следует читать как «компилятор знает , что он может удалить копию», а не как «компилятор будет удалять копию». Пробег зависит от разных компиляторов, и даже один и тот же компилятор может давать разные результаты при разных запусках компиляции (из-за разных параметров или ресурсов, доступных оптимизатору).

В следующем коде будет создан временный файл с суммой a и b, и этот временный код должен быть снова передан в operator+ вместе с c для создания второго временного файла с конечным результатом. :

Q a, b, c;
// initialize values
Q d = a + b + c;

Если operator+ имеет семантику передачи по значению, компилятор может исключить копию передачи по значению (компилятор знает, что временный объект будет разрушен сразу после второго вызова operator+, и ему не нужно создавать другая копия для передачи)

Даже если operator? может быть реализован как однострочная функция (Q operator+( Q lhs, Q const & rhs ) { return lhs+=rhs; }) в коде, это не должно быть так. Причина в том, что компилятор не может знать, является ли ссылка, возвращаемая operator?=, на самом деле ссылкой на тот же объект или нет. Если оператор return явно принимает объект lhs, компилятор знает, что возвращаемую копию можно удалить.

Симметрия относительно типов

Если существует неявное преобразование из типа T в тип Q, и у вас есть два экземпляра t и q соответственно каждого типа, то вы ожидаете, что (t+q) и (q+t) оба будут отозваны. Если вы реализуете operator+ как функцию-член внутри Q, то компилятор не сможет преобразовать объект t во временный объект Q, а затем вызвать (Q(t)+q), поскольку он не может выполнять преобразования типов в левая сторона для вызова функции-члена. Таким образом, с реализацией функции-члена t+q не будет компилироваться.

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

2 голосов
/ 21 августа 2009

Если эти две реализации не генерируют точно один и тот же код сборки, когда включена оптимизация, вам следует рассмотреть возможность использования другого компилятора. :) И я не думаю, что имеет значение, является ли функция встроенной или нет.

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

...