Есть ли разница в производительности между i ++ и ++ i в C ++? - PullRequest

Ответы [ 18 ]

394 голосов
/ 24 августа 2008

[Резюме: используйте ++i, если у вас нет особых причин использовать i++.]

Для C ++ ответ немного сложнее.

Если i - простой тип (не экземпляр класса C ++), , тогда ответ для C («Нет, нет разницы в производительности») сохраняется, так как компилятор генерирует код.

Однако, если i является экземпляром класса C ++, тогда i++ и ++i выполняют вызовы одной из функций operator++. Вот стандартная пара этих функций:

Foo& Foo::operator++()   // called for ++i
{
    this->data += 1;
    return *this;
}

Foo Foo::operator++(int ignored_dummy_value)   // called for i++
{
    Foo tmp(*this);   // variable "tmp" cannot be optimized away by the compiler
    ++(*this);
    return tmp;
}

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

56 голосов
/ 24 августа 2008

Да. Есть.

Оператор ++ может быть или не быть определен как функция. Для примитивных типов (int, double, ...) операторы встроены, поэтому компилятор, вероятно, сможет оптимизировать ваш код. Но в случае объекта, который определяет оператор ++, все иначе.

Функция operator ++ (int) должна создавать копию. Это связано с тем, что postfix ++ должен возвращать значение, отличное от того, что он содержит: он должен хранить свое значение в переменной temp, увеличивать его значение и возвращать temp В случае оператора ++ () с префиксом ++ нет необходимости создавать копию: объект может увеличивать сам себя, а затем просто возвращать себя.

Вот иллюстрация точки:

struct C
{
    C& operator++();      // prefix
    C  operator++(int);   // postfix

private:

    int i_;
};

C& C::operator++()
{
    ++i_;
    return *this;   // self, no copy created
}

C C::operator++(int ignored_dummy_value)
{
    C t(*this);
    ++(*this);
    return t;   // return a copy
}

Каждый раз, когда вы вызываете operator ++ (int), вы должны создавать копию, и компилятор ничего не может с этим поделать. Когда предоставляется выбор, используйте оператор ++ (); Таким образом, вы не сохраняете копию. Это может быть важно в случае большого количества приращений (большой цикл?) И / или больших объектов.

41 голосов
/ 01 марта 2012

Вот пример для случая, когда операторы приращения находятся в разных единицах перевода. Компилятор с g ++ 4.5.

На данный момент игнорируем проблемы со стилем

// a.cc
#include <ctime>
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};

int main () {
    Something s;

    for (int i=0; i<102430; ++i) ++s; // warm up
    std::clock_t a = clock();
    for (int i=0; i<1024*1024*30; ++i) ++s;
    a = clock() - a;

    for (int i=0; i<1024*1024*30; ++i) s++; // warm up
    std::clock_t b = clock();
    for (int i=0; i<1024*1024*30; ++i) s++;
    b = clock() - b;

    std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
              << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
    return 0;
}

O (n) приращение

Test

// b.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    for (auto it=data.begin(), end=data.end(); it!=end; ++it)
        ++*it;
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

Результаты

Результаты (время в секундах) с g ++ 4.5 на виртуальной машине:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

O (1) приращение

Тест

Давайте теперь возьмем следующий файл:

// c.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}
*1024* Это ничего не делает в приращении. Это моделирует случай, когда приращение имеет постоянную сложность.

Результаты

Результаты теперь сильно различаются:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

Заключение

Производительность мудр

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

Семантический-накрест

  • i++ говорит increment i, I am interested in the previous value, though.
  • ++i говорит increment i, I am interested in the current value или increment i, no interest in the previous value. Опять же, вы привыкнете к этому, даже если не сейчас.

Кнут.

Преждевременная оптимизация - корень всего зла. Как и преждевременная пессимизация.

20 голосов
/ 24 августа 2008

Не совсем правильно говорить, что компилятор не может оптимизировать удаление временной переменной в случае постфикса. Быстрый тест с VC показывает, что он, по крайней мере, может сделать это в определенных случаях.

В следующем примере сгенерированный код идентичен для префикса и постфикса, например:

#include <stdio.h>

class Foo
{
public:

    Foo() { myData=0; }
    Foo(const Foo &rhs) { myData=rhs.myData; }

    const Foo& operator++()
    {
        this->myData++;
        return *this;
    }

    const Foo operator++(int)
    {
        Foo tmp(*this);
        this->myData++;
        return tmp;
    }

    int GetData() { return myData; }

private:

    int myData;
};

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

    int count;
    printf("Enter loop count: ");
    scanf("%d", &count);

    for(int i=0; i<count; i++)
    {
        testFoo++;
    }

    printf("Value: %d\n", testFoo.GetData());
}

Если вы используете ++ testFoo или testFoo ++, вы все равно получите тот же самый результирующий код. На самом деле, не считывая счет от пользователя, оптимизатор сводил все это к константе. Итак, это:

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

Приведено следующее:

00401000  push        0Ah  
00401002  push        offset string "Value: %d\n" (402104h) 
00401007  call        dword ptr [__imp__printf (4020A0h)] 

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

13 голосов
/ 29 августа 2008

Руководство по стилю Google C ++ говорит:

Преинкремент и предкремент

Использовать префиксную форму (++ i) операторов увеличения и уменьшения с итераторы и другие объекты шаблона.

Определение: Когда переменная увеличивается (++ i или i ++) или уменьшается (--i или i--) и значение выражения не используется, нужно решить преинкремент (декремент) или постинкремент (декремент).

Плюсы: Когда возвращаемое значение игнорируется, форма "pre" (++ i) никогда не бывает меньше эффективнее, чем форма "post" (i ++), и часто более эффективна. Это связано с тем, что после увеличения (или уменьшения) требуется копия i для быть сделано, что является значением выражения. Если я итератор или другой нескалярный тип, копирование у меня может быть дорогим. Поскольку два типы приращения ведут себя одинаково, когда значение игнорируется, почему бы и нет просто всегда предварительное увеличение?

Минусы: В С развита традиция использования постинкремента, когда Значение выражения не используется, особенно в циклах for. Некоторые находят пост-приращение легче читать, так как «субъект» (i) предшествует "глагол" (++), как в английском.

Решение: Для простых скалярных (необъектных) значений нет причин предпочитать одно Форма и мы разрешаем либо. Для итераторов и других типов шаблонов используйте Преинкремент.

5 голосов
/ 03 сентября 2008

@ Кетан

... поднимает подробности о намерениях и производительности. Иногда мы хотим использовать iter ++ вместо ++ iter.

Очевидно, что post и pre-increment имеют разную семантику, и я уверен, что все согласны с тем, что при использовании результата вы должны использовать соответствующий оператор. Я думаю, что вопрос в том, что делать, когда результат отбрасывается (как в for циклах). Ответ на этот вопрос (ИМХО) заключается в том, что, поскольку соображения производительности в лучшем случае ничтожны, вы должны делать то, что более естественно. Для меня ++i более естественно, но мой опыт подсказывает, что я в меньшинстве, и использование i++ приведет к меньшим затратам металла для большинства людей, читающих ваш код.

В конце концов, по этой причине язык не называется "++C". [*]

[*] Включить обязательное обсуждение о том, что ++C является более логичным именем.

5 голосов
/ 03 сентября 2008

Я хотел бы отметить отличный пост Эндрю Кенига о Code Talk совсем недавно.

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

В нашей компании мы также используем соглашение ++ iter для согласованности и производительности, где это применимо. Но Эндрю поднимает упущенные детали относительно намерения против производительности. Иногда мы хотим использовать iter ++ вместо ++ iter.

Итак, сначала определитесь с намерением, и если pre или post не имеют значения, тогда переходите к pre, так как это принесет некоторую выгоду производительности, избегая создания дополнительного объекта и выбрасывая его.

3 голосов
/ 04 апреля 2017
  1. ++ i - быстрее не используется возвращаемое значение
  2. i ++ - быстрее с использованием возвращаемого значения

Когда не использует возвращаемое значение, компилятор гарантированно не использует временное значение в случае ++ i . Не гарантируется, что будет быстрее, но гарантированно не будет медленнее.

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

3 голосов
/ 16 сентября 2008

Марк: Просто хотел бы отметить, что операторы ++ являются хорошими кандидатами для встраивания, и если компилятор решит это сделать, избыточная копия будет исключена в большинстве случаев. (например, типы POD, которые обычно являются итераторами.)

Тем не менее, в большинстве случаев все еще лучше использовать ++ iter. : -)

3 голосов
/ 06 ноября 2012

Разница в производительности между ++i и i++ будет более очевидной, когда вы будете думать об операторах как о функциях, возвращающих значения, и о том, как они реализованы. Чтобы было проще понять, что происходит, в следующих примерах кода будет использоваться int, как если бы это было struct.

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

int& int::operator++() { 
     return *this += 1;
}

Но то же самое нельзя сказать о i++.

Постинкремент, i++, часто рассматривается как возвращение исходного значения до увеличения . Однако функция может возвращать результат только после его завершения . В результате возникает необходимость создать копию переменной, содержащей исходное значение, увеличить значение переменной, а затем вернуть копию, содержащую исходное значение:

int int::operator++(int& _Val) {
    int _Original = _Val;
    _Val += 1;
    return _Original;
}

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

...