Сведения о вопросе

Faridun

22:37, 6th August, 2020

Теги

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

Просмотров: 558   Ответов: 18

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

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



  Сведения об ответе

piter

14:28, 1st August, 2020

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


  Сведения об ответе

baggs

23:06, 3rd August, 2020

Да. Есть.

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

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

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


  Сведения об ответе

fo_I_K

02:34, 24th August, 2020

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

Кнут.

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


  Сведения об ответе

prince

14:15, 18th August, 2020

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

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


  Сведения об ответе

lool

17:52, 1st August, 2020

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

Преинкремент и Прекрементация

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

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

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

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

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


  Сведения об ответе

9090

18:12, 20th August, 2020

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

http://dobbscodetalk.com/index.php?вариант=com_myblog&show=эффективность против intent.html&Itemid=29

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

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


  Сведения об ответе

PAGE

23:18, 14th August, 2020

@Ketan ..

.

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

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

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

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


  Сведения об ответе

ASSembler

02:52, 28th August, 2020

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

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


  Сведения об ответе

DINO

08:35, 9th August, 2020

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

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

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

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

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

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

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


  Сведения об ответе

dump

18:08, 29th August, 2020

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

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

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


  Сведения об ответе

prince

15:59, 28th August, 2020

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

Обычный ответ заключается в том, что ++i быстрее, чем i++, и, несомненно, это так, но более важный вопрос-"when should you care?"

Если доля времени CPU, затраченного на инкрементирующие итераторы, меньше, чем 10%,, то вам может быть все равно.

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

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


  Сведения об ответе

VERSUION

02:20, 9th August, 2020

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


  Сведения об ответе

fo_I_K

13:01, 8th August, 2020

@wilhelmtell

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

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

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


  Сведения об ответе

davran

16:28, 29th August, 2020

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

О преждевременной оптимизации часто цитируют кнута. - Вот именно. но Дональд Кнут никогда не стал бы защищать этим ужасный кодекс, который вы можете видеть в наши дни. Когда-нибудь видели a = b + c среди Java целых чисел (не int)? Это составляет 3 конверсии бокса/распаковки. Очень важно избегать подобных вещей. И бесполезно писать i++ вместо ++i - это та же самая ошибка. EDIT: как прекрасно выразился фреснель в комментарии, Это можно подвести под "premature optimization is evil, as is premature pessimization".

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

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


  Сведения об ответе

baggs

06:23, 19th August, 2020

Время, чтобы предоставить людям драгоценные камни мудрости ;) - есть простой трюк, чтобы заставить C++ postfix increment вести себя примерно так же, как и prefix increment (придумал это для себя, но видел это также и в коде других людей, так что я не одинок).

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

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


  Сведения об ответе

darknet

11:23, 8th August, 2020

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

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

#include <stdio.h>

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

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

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


  Сведения об ответе

SSESION

04:28, 10th August, 2020

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

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


  Сведения об ответе

darknet

04:08, 26th August, 2020

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

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

#include <iostream>
using namespace std;

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


Ответить на вопрос

Чтобы ответить на вопрос вам нужно войти в систему или зарегистрироваться