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

StackOverflow https://stackoverflow.com/questions/24901

Вопрос

У нас есть вопрос есть ли разница в производительности между i++ и ++i в С?

Какой ответ на C++?

Это было полезно?

Решение

[Управляющее резюме:Использовать ++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 переменная и связанный с ней конструктор копирования.Если конструктор копирования дорогой, это может существенно повлиять на производительность.

Другие советы

Да.Есть.

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

Функция оператора++(int) должна создать копию.Это связано с тем, что postfix ++, как ожидается, вернет значение, отличное от того, которое он содержит:он должен хранить свое значение во временной переменной, увеличивать его значение и возвращать значение 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
}

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

Вот ориентир для случая, когда операторы приращения находятся в разных единицах перевода.Компилятор с 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<1024*1024*30; ++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) приращение

Тест

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

Он ничего не делает при приращении.Это моделирует случай, когда приращение имеет постоянную сложность.

Полученные результаты

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

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.Опять же, вы к этому привыкнете, даже если сейчас это не так.

Кнут.

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

Не совсем корректно говорить, что компилятор не может оптимизировать копию временной переменной в постфиксном случае.Быстрый тест 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)] 

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

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

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

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

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

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

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

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

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

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

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

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

@Кетан

... поднимает упущенные из виду детали относительно намерения и производительности.Бывают случаи, когда мы хотим использовать iter++ вместо ++iter.

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

Ведь именно поэтому язык не называется"++C".[*]

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

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

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

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

  1. ++я - Быстрее не используется возвращаемое значение
  2. я++ - Быстрее с использованием возвращаемое значение

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

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

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

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

Обычный ответ заключается в том, что ++i быстрее, чем i++, и это, без сомнения, так и есть, но более важный вопрос: «Когда это вас должно волновать?»

Если доля времени ЦП, затрачиваемого на увеличение итераторов, составляет менее 10 %, вам может быть все равно.

Если доля времени ЦП, затрачиваемого на увеличение итераторов, превышает 10 %, вы можете посмотреть, какие операторы выполняют эту итерацию.Посмотрите, можно ли просто увеличивать целые числа вместо использования итераторов.Скорее всего, вы могли бы, и хотя это может быть в некотором смысле менее желательно, шансы довольно высоки, вы сэкономите практически все время, потраченное на эти итераторы.

Я видел пример, где приращение итератора занимало более 90% времени.В этом случае переход к целочисленному приращению существенно сокращает время выполнения.(т.е.лучше, чем ускорение в 10 раз)

Предполагаемый вопрос был о том, когда результат не используется (это ясно из вопроса для C).Может ли кто-нибудь это исправить, поскольку вопрос в «вики-сообществе»?

По поводу преждевременной оптимизации часто цитируют Кнута.Это верно.но Дональд Кнут никогда бы не стал защищать этим ужасный кодекс, который вы можете видеть в наши дни.Вы когда-нибудь видели a = b + c среди целых чисел Java (не int)?Это соответствует 3 конверсиям упаковки/распаковки.Очень важно избегать подобных вещей.И бесполезное написание i++ вместо ++i — та же ошибка.РЕДАКТИРОВАТЬ:Как прекрасно выразился Френель в комментарии, это можно резюмировать как «преждевременная оптимизация — это зло, как и преждевременная пессимизация».

Даже тот факт, что люди больше привыкли к i++, является неудачным наследием C, вызванным концептуальной ошибкой K&R (если следовать аргументу намерения, это логический вывод;и защищать K&R, потому что они K&R, бессмысленно, они великолепны, но они не хороши как разработчики языков;существует бесчисленное количество ошибок в дизайне C, начиная от get() и strcpy() и заканчивая API strncpy() (с самого первого дня он должен был иметь API strlcpy()).

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

@wilhelmtell

Компилятор может исключить временное.Дословно из другой темы:

Компилятору C++ разрешено исключать временные объекты на основе стека, даже если это меняет поведение программы.Ссылка MSDN для VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

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

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

#include <iostream>

class Data {
    private: class DataIncrementer {
        private: Data& _dref;

        public: DataIncrementer(Data& d) : _dref(d) {}

        public: ~DataIncrementer() {
            ++_dref;
        }
    };

    private: int _data;

    public: Data() : _data{0} {}

    public: Data(int d) : _data{d} {}

    public: Data(const Data& d) : _data{ d._data } {}

    public: Data& operator=(const Data& d) {
        _data = d._data;
        return *this;
    }

    public: ~Data() {}

    public: Data& operator++() { // prefix
        ++_data;
        return *this;
    }

    public: Data operator++(int) { // postfix
        DataIncrementer t(*this);
        return *this;
    }

    public: operator int() {
        return _data;
    }
};

int
main() {
    Data d(1);

    std::cout <<   d << '\n';
    std::cout << ++d << '\n';
    std::cout <<   d++ << '\n';
    std::cout << d << '\n';

    return 0;
}

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

Оба столь же быстры;) Если вы хотите, чтобы это был один и тот же расчет для процессора, это просто порядок, в котором он выполняется, который отличается.

Например, следующий код:

#include <stdio.h>

int main()
{
    int a = 0;
    a++;
    int b = 0;
    ++b;
    return 0;
}

Произведите следующую сборку:

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    $0x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

Вы видите, что для a++ и b++ это мнемоника incl, так что это одна и та же операция;)

Когда ты пишешь i++ вы говорите компилятору увеличивать значение после завершения этой строки или цикла.

++i немного отличается от i++i++ вы увеличиваете после завершения цикла, но ++i вы увеличиваете значение непосредственно перед завершением цикла.

++i быстрее, чем i++ потому что он не возвращает старую копию значения.

Это также более интуитивно понятно:

x = i++;  // x contains the old value of i
y = ++i;  // y contains the new value of i 

Этот пример C печатает «02» вместо «12», как вы могли ожидать:

#include <stdio.h>

int main(){
    int a = 0;
    printf("%d", a++);
    printf("%d", ++a);
    return 0;
}

То же самое для С++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top