Есть ли разница в производительности между i++ и ++i в C++?
-
09-06-2019 - |
Вопрос
У нас есть вопрос есть ли разница в производительности между 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
задействован конструктор копирования, который будет вызываться при постинкременте, и выполнить эту оптимизацию будет невозможно, если потребуется глубокое копирование.Таким образом, преинкремент обычно выполняется быстрее и требует меньше памяти, чем постинкремент.
- ++я - Быстрее не используется возвращаемое значение
- я++ - Быстрее с использованием возвращаемое значение
Когда не используется возвращаемое значение, компилятор гарантированно не будет использовать временное значение в случае ++я.Не гарантированно будет быстрее, но гарантированно не будет медленнее.
Когда с использованием возвращаемое значение я++ Позволяет процессору протолкнуть как приращение, так и левую сторону в трубопровод, поскольку они не зависят друг от друга.++Я могу остановить конвейер, потому что процессор не сможет запустить левую часть, пока операция предварительного приращения не пройдет весь путь.Опять же, остановка конвейера не гарантирована, поскольку процессор может найти и другие полезные вещи, которые можно вставить.
Причина, по которой вам следует использовать ++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;
}