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

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

Какой ответ для С++?

Ответы

Ответ 1

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

Ответ 2

Да. Существует.

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

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

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

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), вы должны создать копию, и компилятор ничего не сможет с этим поделать. При выборе варианта используйте оператор ++(); таким образом вы не сохраняете копию. Это может быть значительным в случае многих приращений (большой цикл?) И/или больших объектов.

Ответ 3

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

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) приращение

Test

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

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

Заключение

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

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

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

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

Кнут.

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

Ответ 4

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

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

Ответ 5

Руководство по стилю Google С++:

Preincrement and Prementrement

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

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

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

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

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

Ответ 6

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

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

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

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

Ответ 7

@Ketan

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

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

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

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

Ответ 8

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

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

Ответ 9

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

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

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

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

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

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

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

Ответ 10

  • ++ i - быстрее не использует возвращаемое значение
  • я ++ - быстрее , используя возвращаемое значение

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

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

Ответ 11

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

Ответ 12

@wilhelmtell

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

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

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

Ответ 13

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

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

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

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

Ответ 14

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

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

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

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

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

Ответ 15

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

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

#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 ++ это тоже мнемонично, так что это одна и та же операция;)

Ответ 16

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

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

Ответ 17

Время, чтобы предоставить людям драгоценные камни мудрости;) - есть простой трюк, чтобы сделать приращение постфикса 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;
}

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

Ответ 18

Это все очень глупо...
Кто еще предположил, что компилятор просто ИСПОЛЬЗУЕТ его, а затем обновит, или ОБНОВЛЕНО, а затем использовал его? Имеется в виду отсутствие дублирующих методов ++... Но, конечно, это дает программисту больше ярлыков программирования, чем... (иначе вам не нужно использовать ++pre и post++ с одинаковым значением. Но опять же, это не так трудно просто написать имя метода в любом случае. LOL)

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

Просто используйте его самостоятельно, а затем обновите его. не более post++ !! Это мой совет. ;)

(PS Я понимаю, что это больше комментарий, но это полезный ответ)

Ответ 19

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