I ++ менее эффективен, чем ++ i, как это показать?
Я пытаюсь показать на примере, что приращение префикса более эффективно, чем приращение постфикса.
В теории это имеет смысл: я ++ должен уметь возвращать неосновное исходное значение и, следовательно, хранить его, тогда как ++ я может возвращать добавочное значение без сохранения предыдущего значения.
Но есть ли хороший пример, чтобы продемонстрировать это на практике?
Я попробовал следующий код:
int array[100];
int main()
{
for(int i = 0; i < sizeof(array)/sizeof(*array); i++)
array[i] = 1;
}
Я скомпилировал его с помощью gcc 4.4.0 следующим образом:
gcc -Wa,-adhls -O0 myfile.cpp
Я сделал это снова, при этом приращение postfix изменилось на приращение префикса:
for(int i = 0; i < sizeof(array)/sizeof(*array); ++i)
В обоих случаях получается идентичный код сборки.
Это было несколько неожиданно. Похоже, что отключив оптимизацию (с -O0), я должен увидеть разницу, чтобы показать концепцию. Что мне не хватает? Есть ли лучший пример, чтобы показать это?
Ответы
Ответ 1
В общем случае приращение по почте приведет к копированию, где предварительный приращение не будет. Конечно, это будет оптимизировано во многих случаях, а в случаях, когда операция копирования не будет незначительной (т.е. Для встроенных типов).
Вот небольшой пример, показывающий потенциальную неэффективность пост-приращения.
#include <stdio.h>
class foo
{
public:
int x;
foo() : x(0) {
printf( "construct foo()\n");
};
foo( foo const& other) {
printf( "copy foo()\n");
x = other.x;
};
foo& operator=( foo const& rhs) {
printf( "assign foo()\n");
x = rhs.x;
return *this;
};
foo& operator++() {
printf( "preincrement foo\n");
++x;
return *this;
};
foo operator++( int) {
printf( "postincrement foo\n");
foo temp( *this);
++x;
return temp;
};
};
int main()
{
foo bar;
printf( "\n" "preinc example: \n");
++bar;
printf( "\n" "postinc example: \n");
bar++;
}
Результаты оптимизированной сборки (которая фактически удаляет вторую операцию копирования в случае после инкремента из-за RVO):
construct foo()
preinc example:
preincrement foo
postinc example:
postincrement foo
copy foo()
В общем случае, если вам не нужна семантика пост-инкремента, зачем использовать случайную копию?
Конечно, хорошо иметь в виду, что пользовательский оператор ++() - либо pre, либо post-вариант - может свободно возвращать то, что он хочет (или даже делать все, что он хочет), и я бы предположил, что есть довольно много, которые не следуют обычным правилам. Иногда я сталкивался с реализациями, которые возвращают "void
", что делает обычную смысловую разницу отсутствующей.
Ответ 2
Вы не увидите никакой разницы с целыми числами. Вам нужно использовать итераторы или что-то там, где post и prefix действительно делают что-то другое. И вам нужно включить все оптимизации на, а не выключить!
Ответ 3
Мне нравится следовать правилу "скажи, что ты имеешь в виду".
++i
просто увеличивается. i++
увеличивает , а имеет специальный, неинтуитивный результат оценки. Я использую только i++
, если я явно хочу этого поведения, и использую ++i
во всех остальных случаях. Если вы следуете этой практике, когда вы видите код i++
в коде, очевидно, что поведение post-increment действительно предназначалось.
Ответ 4
Несколько точек:
- Во-первых, вы вряд ли увидите существенное различие в производительности.
- Во-вторых, ваш бенчмаркинг бесполезен, если вы отключили оптимизацию. Мы хотим знать, что это изменение дает нам более или менее эффективный код, а это значит, что мы должны использовать его с наиболее эффективным кодом, который может произвести компилятор. Нам все равно, быстрее ли это в неоптимизированных сборках, нам нужно знать, быстрее ли они оптимизированы.
- Для встроенных типов данных, таких как целые числа, компилятор обычно может оптимизировать разницу. Проблема в основном возникает для более сложных типов с перегруженными итераторами приращения, где компилятор не может тривиально видеть, что эти две операции будут эквивалентны в контексте.
- Вы должны использовать код, который четко выражает ваши намерения. Вы хотите "добавить один к значению" или "добавить его к значению, но продолжать работать над первоначальным значением немного дольше"? Обычно это первый случай, и тогда предварительный прирост лучше выражает ваши намерения.
Если вы хотите показать разницу, самым простым вариантом является простое обращение обоих операторов и укажите, что требуется дополнительная копия, а другая нет.
Ответ 5
Попробуйте использовать while или сделать что-то с возвращаемым значением, например:
#define SOME_BIG_CONSTANT 1000000000
int _tmain(int argc, _TCHAR* argv[])
{
int i = 1;
int d = 0;
DWORD d1 = GetTickCount();
while(i < SOME_BIG_CONSTANT + 1)
{
d += i++;
}
DWORD t1 = GetTickCount() - d1;
printf("%d", d);
printf("\ni++ > %d <\n", t1);
i = 0;
d = 0;
d1 = GetTickCount();
while(i < SOME_BIG_CONSTANT)
{
d += ++i;
}
t1 = GetTickCount() - d1;
printf("%d", d);
printf("\n++i > %d <\n", t1);
return 0;
}
Скомпилированный с помощью VS 2005 с использованием /O 2 или/Ox, попробовал на моем рабочем столе и на ноутбуке.
Стабильно что-то на ноутбуке, на настольных компьютерах немного отличается (но скорость примерно такая же):
i++ > 8xx <
++i > 6xx <
xx означает, что числа различны, например. 813 против 640 - все еще около 20% ускоряется.
И еще один момент - если вы замените "d + =" на "d =", вы увидите хороший трюк оптимизации:
i++ > 935 <
++i > 0 <
Однако это довольно специфично. Но в конце концов, я не вижу причин, чтобы изменить свое мнение и думаю, что нет никакой разницы:)
Ответ 6
Этот код и его комментарии должны демонстрировать различия между ними.
class a {
int index;
some_ridiculously_big_type big;
//etc...
};
// prefix ++a
void operator++ (a& _a) {
++_a.index
}
// postfix a++
void operator++ (a& _a, int b) {
_a.index++;
}
// now the program
int main (void) {
a my_a;
// prefix:
// 1. updates my_a.index
// 2. copies my_a.index to b
int b = (++my_a).index;
// postfix
// 1. creates a copy of my_a, including the *big* member.
// 2. updates my_a.index
// 3. copies index out of the **copy** of my_a that was created in step 1
int c = (my_a++).index;
}
Вы можете видеть, что postfix имеет дополнительный шаг (шаг 1), который включает в себя создание копии объекта. Это имеет как последствия как для потребления памяти, так и для времени выполнения. Это, почему префикс более эффективен для постфикса для не-базовых типов.
В зависимости от some_ridiculously_big_type
, а также от того, что вы делаете с результатом incrememt, вы сможете увидеть разницу с оптимизацией или без нее.
Ответ 7
В ответ на Mihail это несколько более портативная версия его кода:
#include <cstdio>
#include <ctime>
using namespace std;
#define SOME_BIG_CONSTANT 100000000
#define OUTER 40
int main( int argc, char * argv[] ) {
int d = 0;
time_t now = time(0);
if ( argc == 1 ) {
for ( int n = 0; n < OUTER; n++ ) {
int i = 0;
while(i < SOME_BIG_CONSTANT) {
d += i++;
}
}
}
else {
for ( int n = 0; n < OUTER; n++ ) {
int i = 0;
while(i < SOME_BIG_CONSTANT) {
d += ++i;
}
}
}
int t = time(0) - now;
printf( "%d\n", t );
return d % 2;
}
Здесь есть внешние контуры, позволяющие мне настраивать тайминги, чтобы получить что-то подходящее на моей платформе.
Я больше не использую VС++, поэтому я скомпилировал его (в Windows) с помощью
g++ -O3 t.cpp
Затем я запустил его, чередуя:
a.exe
и
a.exe 1
Мои результаты по времени были примерно одинаковыми для обоих случаев. Иногда одна версия будет быстрее на 20%, а иногда и на другую. Это, я думаю, связано с другими процессами, запущенными в моей системе.
Ответ 8
Возможно, вы могли бы просто показать теоретическое различие, написав обе версии с инструкциями по сборке x86? Как указывало многие люди, компилятор всегда будет принимать собственные решения о том, как лучше всего скомпилировать/собрать программу.
Если этот пример предназначен для учащихся, не знакомых с набором инструкций x86, вы можете использовать набор инструкций MIPS32 - по какой-то нечетной причине многие считают, что это легче понять, чем сборка x86.
Ответ 9
Хорошо, вся эта оптимизация "префикс/постфикс" - это просто... какое-то большое недоразумение.
Основная идея, что я ++ возвращает исходную копию и, следовательно, требует копирования значения.
Это может быть правильным для некоторых неэффективных реализаций итераторов. Однако в 99% случаев даже с итераторами STL нет никакой разницы, потому что компилятор знает, как его оптимизировать, а фактические итераторы - это просто указатели, которые выглядят как класс. И, конечно, нет никакой разницы для примитивных типов, таких как целые числа в указателях.
Итак... забудь об этом.
ИЗМЕНИТЬ: Очистка
Как я уже упоминал, большинство классов итератора STL - это просто указатели, завернутые в классы, которые имеют все функции-члены inlined оптимизация такой нерелевантной копии.
И да, если у вас есть свои итераторы без встроенных функций-членов, то это может
работа медленнее. Но вы должны просто понять, что делает компилятор, а что нет.
Как небольшое доказательство, возьмите этот код:
int sum1(vector<int> const &v)
{
int n;
for(auto x=v.begin();x!=v.end();x++)
n+=*x;
return n;
}
int sum2(vector<int> const &v)
{
int n;
for(auto x=v.begin();x!=v.end();++x)
n+=*x;
return n;
}
int sum3(set<int> const &v)
{
int n;
for(auto x=v.begin();x!=v.end();x++)
n+=*x;
return n;
}
int sum4(set<int> const &v)
{
int n;
for(auto x=v.begin();x!=v.end();++x)
n+=*x;
return n;
}
Скомпилируйте его для сборки и сравнения sum1 и sum2, sum3 и sum4...
Я просто могу сказать вам... gcc дает точно такой же код с -02
.