У нас есть вопрос, есть ли разница в производительности между i++
и ++i
в C ?
Какой ответ для C ++?
У нас есть вопрос, есть ли разница в производительности между i++
и ++i
в C ?
Какой ответ для 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, ...) операторы встроены, поэтому компилятор, вероятно, сможет оптимизировать ваш код. Но в случае объекта, который определяет оператор ++, все иначе.
Функция 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), вы должны создавать копию, и компилятор ничего не может с этим поделать. Когда предоставляется выбор, используйте оператор ++ (); Таким образом, вы не сохраняете копию. Это может быть важно в случае многих приращений (большой цикл?) И / или больших объектов.
C t(*this); ++(*this); return t;
Во второй строке вы увеличиваете указатель this вправо, поэтому как t
обновляться, если вы увеличиваете это. Не были ли скопированы значения этого t
?
The operator++(int) function must create a copy.
нет это не так. Не больше копий, чемoperator++()
Вот пример для случая, когда операторы приращения находятся в разных единицах перевода. Компилятор с 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;
}
// 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
Давайте теперь возьмем следующий файл:
// 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
. Опять же, вы привыкнете к этому, даже если вы не сейчас.Преждевременная оптимизация - корень всего зла. Как и преждевременная пессимизация.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
не обращайте внимания на фактическую структуру дерева (BSP, kd, Quadtree, Octree Grid и т. Д.). Такой итератор должен был бы поддерживать какое - то состояние, например parent node
, child node
, index
и тому подобное. В общем, моя позиция такова, даже если существует только несколько примеров ...
Не совсем правильно говорить, что компилятор не может оптимизировать удаление временной переменной в случае постфикса. Быстрый тест с 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 ++ Style Guide говорит:
Прединкремент и предкремент
Используйте префиксную форму (++ i) операторов увеличения и уменьшения с итераторами и другими объектами шаблона.
Определение: когда переменная увеличивается (++ i или i ++) или уменьшается (--i или i--) и значение выражения не используется, необходимо решить, будет ли преинкремент (декремент) или постинкремент (декремент).
Плюсы: Когда возвращаемое значение игнорируется, форма "pre" (++ i) никогда не менее эффективна, чем форма "post" (i ++), и часто более эффективна. Это потому, что постинкремент (или декремент) требует создания копии i, которая является значением выражения. Если я итератор или другой нескалярный тип, копирование может быть дорогим. Поскольку два типа приращения ведут себя одинаково, когда значение игнорируется, почему бы просто не всегда выполнить предварительное приращение?
Минусы: в Си сложилась традиция использовать постинкремент, когда значение выражения не используется, особенно в циклах for. Некоторые считают, что постинкремент легче читать, поскольку «subject» (i) предшествует «глаголу» (++), как и в английском.
Решение: для простых скалярных (необъектных) значений нет причин предпочитать одну форму, и мы разрешаем любую. Для итераторов и других типов шаблонов используйте предварительное увеличение.
Я хотел бы отметить отличный пост Эндрю Кенига о Code Talk совсем недавно.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
В нашей компании мы также используем соглашение ++ iter для согласованности и производительности, где это применимо. Но Эндрю поднимает упущенные детали относительно намерения против производительности. Иногда мы хотим использовать iter ++ вместо ++ iter.
Итак, сначала определитесь с вашим намерением, и если pre или post не имеют значения, тогда переходите к pre, поскольку это принесет некоторую выгоду производительности, избегая создания дополнительного объекта и бросая его.
@Ketan
... поднимает упущенные детали относительно намерения против производительности. Иногда мы хотим использовать iter ++ вместо ++ iter.
Очевидно, что post и pre-increment имеют разную семантику, и я уверен, что все согласны с тем, что при использовании результата вы должны использовать соответствующий оператор. Я думаю, вопрос в том, что делать, когда результат отбрасывается (как в for
циклах). Ответ на этот вопрос (ИМХО) заключается в том, что, поскольку соображения производительности в лучшем случае незначительны, вы должны делать то, что более естественно. Для меня ++i
это более естественно, но мой опыт подсказывает мне, что я в меньшинстве, и использование i++
приведет к снижению затрат металла для большинства людей, читающих ваш код.
Ведь именно поэтому язык не называется "++C
". [*]
[*] Включить обязательное обсуждение ++C
более логичного имени.
Когда не используется возвращаемое значение, компилятор гарантированно не использует временный в случае ++ i . Не гарантируется, что будет быстрее, но гарантированно не будет медленнее.
При использовании возвращаемого значения i ++ позволяет процессору вставлять в конвейер как инкремент, так и левую сторону, поскольку они не зависят друг от друга. ++ Я могу остановить конвейер, потому что процессор не может запустить левую сторону до тех пор, пока операция предварительного приращения не будет извилистой до конца. Опять же, остановка конвейера не гарантируется, так как процессор может найти другие полезные вещи, чтобы застрять.
Марк: Просто хотел бы отметить, что операторы ++ являются хорошими кандидатами для встраивания, и если компилятор решит это сделать, избыточная копия будет исключена в большинстве случаев. (например, типы 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
, конструктор копирования будет вызываться после инкремента, и эта оптимизация будет невозможна, если требуется глубокое копирование. Таким образом, предварительное увеличение обычно происходит быстрее и требует меньше памяти, чем последующее увеличение.
@Mark: я удалил свой предыдущий ответ, потому что это было немного перевернуто, и заслужил понижение для одного этого. Я на самом деле думаю, что это хороший вопрос в том смысле, что он спрашивает, что думают многие люди.
Обычный ответ таков: ++ я быстрее, чем i ++, и, без сомнения, так и есть, но главный вопрос в том, «когда тебя это волнует?».
Если доля процессорного времени, затрачиваемого на инкрементные итераторы, составляет менее 10%, вам может быть все равно.
Если доля процессорного времени, затрачиваемого на увеличение итераторов, превышает 10%, вы можете посмотреть, какие операторы выполняют эту итерацию. Посмотрите, можете ли вы просто увеличивать целые числа, а не использовать итераторы. Скорее всего, вы могли бы, и, хотя это может быть в некотором смысле менее желательно, шансы довольно хороши, вы сэкономите практически все время, проведенное в этих итераторах.
Я видел пример, когда увеличение итератора занимало более 90% времени. В этом случае переход к целочисленному приращению сокращает время выполнения по существу на эту величину. (т.е. лучше, чем 10-кратное ускорение)
@wilhelmtell
Компилятор может исключить временный. Дословно из другой ветки:
Компилятору C ++ разрешено устранять временные эффекты на основе стека, даже если это изменяет поведение программы. Ссылка MSDN для VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Причина, по которой вы должны использовать ++ i даже на встроенных типах, в которых нет никакого преимущества в производительности, заключается в создании хорошей привычки для себя.
И то, и другое так же быстро;) Если вы хотите, чтобы для процессора использовались одни и те же вычисления, различается только порядок их выполнения.
Например, следующий код:
#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 ++ это мнемоника вкл, так что это та же самая операция;)
Намеченный вопрос был о том, когда результат не используется (это ясно из вопроса для C). Кто-нибудь может это исправить, так как вопрос «вики сообщества»?
О преждевременной оптимизации часто цитируют Кнута. Вот так. но Дональд Кнут никогда не защитит с этим ужасным кодом, который вы можете видеть в эти дни. Вы когда-нибудь видели a = b + c среди Java Integer (не int)? Это составляет 3 конверсии в бокс / распаковку. Очень важно избегать подобных вещей. И бесполезно писать i ++ вместо ++ i - это та же ошибка. РЕДАКТИРОВАТЬ: Как хорошо говорит Френель в комментарии, это можно суммировать как «преждевременная оптимизация - это зло, так же как и преждевременная пессимизация».
Даже тот факт, что люди более привыкли к i ++, является печальным наследием C, вызванным концептуальной ошибкой K & R (если вы следуете аргументу намерения, это логичный вывод; и защищать K & R, потому что они K & R, не имеет смысла, они отлично, но они не так хороши, как разработчики языка: в дизайне C существует множество ошибок, от get () до strcpy (), до API strncpy () (он должен был иметь API strlcpy () с первого дня) ).
Кстати, я один из тех, кто недостаточно привык к C ++, чтобы найти ++, которую я раздражаю читать. Тем не менее, я использую это, так как я признаю, что это правильно.
++i
больше раздражающего, чем i++
(на самом деле, я нашел это круче), но остальная часть вашего поста получает мое полное признание. Возможно, добавьте пункт «преждевременная оптимизация - это зло, равно как и преждевременная пессимизация»
strncpy
служил цели в файловых системах, которые они использовали в то время; имя файла было 8-символьным буфером, и оно не должно заканчиваться нулем. Вы не можете винить их за то, что они не видели 40 лет в будущем языковой эволюции.
strlcpy()
было оправдано тем, что оно еще не было изобретено.
Пришло время предоставить людям драгоценные камни мудрости;) - есть простой трюк, чтобы заставить приращение постфикса 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;
}
Изобретен для некоторого тяжелого пользовательского кода итераторов, и он сокращает время выполнения. Стоимость префикса по сравнению с постфиксом теперь является одной ссылкой, и если это пользовательский оператор, выполняющий тяжелые перемещения, префикс и постфикс дают мне одинаковое время выполнения.
++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;
}