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

Эффективность postincrement v.s. preincrement в С++

Обычно я думаю, что преинкремент более эффективен, чем постинкремент в C++. Но когда я недавно прочитал книгу Game Engine Architecture (2nd ed.), в одном из разделов говорится, что постинкремент предпочтительнее, чем преинкремент в цикле for. Потому что, как я цитирую, "preincrement вводит зависимость данных в ваш код - ЦП должен дождаться завершения операции приращения, прежде чем его значение можно будет использовать в выражении". Это правда? (Это действительно подрывает мое представление об этой проблеме.)

Вот цитата из раздела на случай, если вы заинтересованы:

5.3.2.1 Преинкремент против постинкремента

Обратите внимание, что в приведенном выше примере мы используем постинкрементный оператор C++, p++, а не прединкрементный оператор, ++p. Это тонкая, но иногда важная оптимизация. Оператор preincrement увеличивает содержимое переменной до того, как ее (теперь измененное) значение используется в выражении. Оператор postincrement увеличивает содержимое переменной после ее использования. Это означает, что написание ++p вводит зависимость данных в ваш код - ЦП должен ждать завершения операции приращения, прежде чем его значение можно будет использовать в выражении. На глубоко конвейерном процессоре это приводит к остановке. С другой стороны, с p++ нет зависимости от данных. Значение переменной можно использовать немедленно, а операция приращения может произойти позже или параллельно с ее использованием. В любом случае, в pipeопровод не поступает стойло.

Конечно, в выражении 'update' цикла for (for(init_expr; test_expr; update_expr) { ... }) не должно быть различий между pre- и постинкремент. Это потому, что любой хороший компилятор признает, что значение переменной не используется в update_expr. Но в тех случаях, когда значение используется, postincrement лучше, потому что он не вводит срыв в конвейере процессоров. Поэтому, это хорошо, чтобы привыкнуть всегда использовать postincrement, если вам абсолютно не нужна семантика preincrement.

Изменение: Добавить "приведенный выше пример".

void processArray(int container[], int numElements)
{
    int* pBegin = &container[0];
    int* pEnd = &container[numElements];
    for (int* p = pBegin; p != pEnd; p++)
    {
        int element = *p;
        // process element...
    }
}

void processList(std::list<int>& container)
{
    std::list<int>::iterator pBegin = container.begin();
    std::list<int>::iterator pEnd = container.end();
    std::list<inf>::iterator p;
    for (p = pBegin; p != pEnd; p++)
    {
        int element = *p;
        // process element...
    }
}
4b9b3361

Ответ 1

     Предварительное увеличение вводит зависимость данных в ваш код - ЦП должен дождаться завершения операции увеличения, прежде чем его значение можно будет использовать в выражении. "

Это правда?

Это в основном верно - хотя, возможно, слишком строго. Предварительное увеличение не обязательно вводит зависимость данных - но может.

Тривиальный пример для экспозиции:

a = b++ * 2;

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

Другой пример:

a = ++b * 2;

Здесь умножение должно выполняться после приращения, поскольку один из операндов умножения зависит от результата приращения.

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

Практический пример с использованием цикла:

for(int i= 0; arr[i++];)
    count++;

for(int i=-1; arr[++i];) // more typically: (int i=0; arr[i]; ++i;)
    count++;

Можно подумать, что последние обязательно быстрее, если они рассуждают о том, что "постинкремент создает копию" - что было бы очень верно в случае неосновных типов. Однако из-за зависимости данных (а также потому, что int является фундаментальным типом без функции перегрузки для операторов приращения), первый теоретически может быть более эффективным. На самом ли деле это зависит от архитектуры процессора и возможностей оптимизатора.

Что бы это ни стоило - в простой программе на архитектуре x86, использующей компилятор g++ с включенной оптимизацией, вышеприведенные циклы имели одинаковый вывод сборки, поэтому в этом случае они абсолютно эквивалентны.


Правила большого пальца:

Если счетчик является базовым типом и результат приращения не используется, то не имеет значения, используете ли вы post/pre-increment.

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

Если счетчик является базовым типом и используется результат приращения, то теоретически постинкремент может быть несколько более эффективным - в некоторой архитектуре ЦП - в некотором контексте - с использованием некоторого компилятора.

Если счетчик не является базовым типом и используется результат приращения, то предварительное увеличение обычно быстрее, чем последующее увеличение. Также см. ответ р. Саху по этому делу.

Ответ 2

Одна точка данных из моего опыта.

Изменение пост-приращения до предварительного приращения контуров std::map::iterator в for привело к заметной экономии основного кода на моей работе.

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

Оператор pre-increment обычно реализуется как:

typename& typename::operator++()
{
   // Change state
   ...

   // Return the object
   return *this;
}

в то время как оператор post-increment обычно реализуется как:

typename typename::operator++(int)
{
   // Create a temporary object that is a copy of the current object.
   typename temp(*this):

   // Change state of the current object
   ...

   // Return the temporary object.
   return temp;
}