2009-08-21 3 views
7

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

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++ полностью самоучка, когда речь идет о некоторых оптимизаций, я не уверен, что делать, потому что я не знаю точно, как компилятор обрабатывает эти вещи. Также как эти механики переводятся на не-встроенные реализации.

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

+2

Я не знаю, откуда __forceinline, но он, конечно, не является стандартным C++ – 2009-08-21 19:13:16

+0

его компилятор специфичен. Это просто заставляет компилятор сделать его встроенным. – Mark

+0

Мы ничего не можем сказать об этом, не зная, что такое ваш компилятор, поскольку это все зависит от компилятора. –

ответ

10

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

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

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

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

This article about Move Constructors by Andrei Alexandrescu имеет хорошую информацию о том, как работает RVO (и когда и почему компиляторы не могут его использовать).

В том числе этот бит:

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

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

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

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

+2

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

+1

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

6

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

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

+1

И я всегда старался сделать их изменчивыми, исходя из того, что в C++ пользовательские числовые типы должны (насколько это возможно) имитировать int. В частности. Java - это совсем другая история, все ваши переменные являются ссылками и неизменяемыми типами. –

2

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

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

+0

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

+3

Во-первых, написание портативного кода, когда это возможно, обычно является хорошей привычкой. Во-вторых, даже если никто больше не смотрит ваш код, вы можете захотеть использовать его на другой платформе или с другим компилятором когда-нибудь. Принуждение функции к inline, когда большинство компиляторов делают довольно хорошую работу по своему усмотрению, не кажется хорошей причиной для использования нестандартной функции. Только мои 2 цента. :) – Dima

+0

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

2

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

// 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; 
} 

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

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

Эффективность оператора?

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

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

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

Если operator+ имеет значение semantics, то компилятор может исключить экземпляр pass-by-value (компилятор знает, что временное будет разрушено сразу после второго вызова 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 с другой автоматической раскруткой.