i ++ менее эффективен, чем ++ i, как это показать?

Я пытаюсь показать на примере, что приращение префикса более эффективно, чем приращение постфикса.

Теоретически это имеет смысл: 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

Я сделал это снова, изменив приращение постфикса на приращение префикса:

for(int i = 0; i < sizeof(array)/sizeof(*array); ++i)

Результат - идентичный ассемблерный код в обоих случаях.

Это было несколько неожиданно. Казалось, что, отключив оптимизацию (с -O0), я увижу разницу, чтобы показать концепцию. Что мне не хватает? Есть лучший пример, чтобы показать это?


person cschol    schedule 12.07.2009    source источник
comment
Компилятор достаточно умен, чтобы сделать вывод, что ++ i и i ++ дадут одинаковый результат в вашем примере цикла. Попробуйте на самом деле использовать результат, назначив его переменной и вычислив что-нибудь с ней, например, индекс массива или что-то в этом роде. Но я полагаю, вы увидите незначительные различия.   -  person Lasse V. Karlsen    schedule 12.07.2009
comment
кстати: sizeof (array) / sizeof (array [0]) ... sizeof (* array) = sizeof (int)   -  person Artyom    schedule 12.07.2009
comment
Вам не хватает того, что компилятор умнее вас. В вашем примере кода i ++ и ++ i будут иметь одинаковый эффект, несмотря ни на что. Это даже не оптимизация компилятора, это просто нормальная генерация кода. Если вам нужен другой ассемблерный код с отключенной оптимизацией, создайте код, в котором i ++ и ++ i являются частью более крупного выражения. (когда вы снова включите оптимизацию, вы обнаружите, что это не имеет значения). Вы должны тестировать post vs preincrement в C ++, где это может иметь небольшое значение (например, на итераторах)   -  person nos    schedule 13.07.2009
comment
С целым числом вы вряд ли увидите разницу, компилятор может сделать некоторые умные оптимизации вокруг этого. Вы можете обнаружить разницу с пользовательскими типами, для которых правильно определены пре и пост приращения (т. Е. Они ведут себя как встроенные типы).   -  person Martin York    schedule 13.07.2009
comment
@Artyom: Да, конечно, но если вы когда-нибудь решите изменить тип, который array хранит в своих элементах, вы, скорее всего, столкнетесь с ошибкой, если будете использовать sizeof(int) вместо sizeof(*array), поскольку велика вероятность, что вы также забудете изменить на этом месте. Код постоянно меняется, поэтому всегда стремитесь упростить внесение в него изменений, что обычно означает сокращение количества мест, которые вам нужно изменить в коде, чтобы изменения произошли, особенно если это места. который будет компилироваться, даже если вы их не измените (как в этом случае).   -  person HelloGoodbye    schedule 07.01.2014


Ответы (9)


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

Вот небольшой пример, показывающий потенциальную неэффективность постинкремента.

#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()

В общем, если вам не нужна семантика постинкремента, зачем рисковать, что произойдет ненужная копия?

Конечно, хорошо иметь в виду, что пользовательский оператор ++ () - вариант до или после - может возвращать все, что хочет (или даже делать все, что хочет), и я предполагаю, что существует довольно много которые не следуют обычным правилам. Иногда мне попадались реализации, возвращающие "void", что устраняет обычную семантическую разницу.

person Michael Burr    schedule 12.07.2009
comment
Этот пример ясно иллюстрирует проблему i ++ - если i - сложный тип, где копирование стоит дорого (возможно, сложный итератор с подсчетом ссылок), то копирования следует избегать. - person Tom Leys; 13.07.2009

Вы не увидите разницы с целыми числами. Вам нужно использовать итераторы или что-то еще, где post и prefix действительно делают что-то другое. И вам нужно включить все оптимизации, включить, а не выключить!

person Community    schedule 12.07.2009
comment
Не могли бы вы уточнить этот комментарий по оптимизации? Спасибо! - person cschol; 12.07.2009
comment
Каждый раз при тестировании чего-либо вам нужно заставить компилятор сгенерировать лучший код, который он может. при низком уровне оптимизации всевозможные факторы, такие как плохое использование регистров, могут влиять на результаты и искажать результаты. Меня это укусило, когда я раньше публиковал здесь тесты. - person ; 13.07.2009
comment
Я думаю, он просто хочет увидеть, что без оптимизации ++x должен генерировать более быстрый код, чем x++. - person GManNickG; 13.07.2009
comment
@GMan Я хочу сказать, что без оптимизации другие факторы могут помешать продемонстрировать это, даже если бы это было правдой в идеальном мире. - person ; 13.07.2009
comment
+1 краткий, полный ответ. специально для того, чтобы упомянуть ловушку оптимизации. - person mmx; 13.07.2009
comment
@Mihail Нет, наверное, нет. Компилятор может создать идентичный код для двух образцов, отправленных опрашивающим, но, вероятно, не будет. Один будет немного отличаться по скорости от другого с учетом всех оптимизаций. Такова жизнь. Вопрос в том, являются ли такие различия значительными и воспроизводимыми? - person ; 13.07.2009
comment
@Nail: Да, это воспроизводимо, и на моем компьютере скорость увеличивается примерно на 20% - но это на тот случай, если мы не делаем ничего, кроме ++, поэтому он может показать, что ++ i быстрее. Но в реальной жизни слишком сложно найти задачи, при выполнении которых можно было бы получить ощутимую пользу - это правда. - person Mikhail Churbanov; 13.07.2009
comment
@Mihail Если вы видите разницу в 20%, я предлагаю вам опубликовать свой код вместе с вашим протоколом тестирования (как он был скомпилирован, как запускались разные версии (и как часто и в каком порядке), как это было рассчитано по времени и т. .) как ТАК вопрос. Разницы в 20% быть не должно. - person ; 13.07.2009
comment
@Neil: извините за опечатку в вашем имени в предыдущем комментарии. И да, я уже разместил код в своем ответе. Смотри вниз. И я буду рад, если вы поможете указать, в чем проблема в моем примере - ненавижу расплывчатость ... - person Mikhail Churbanov; 13.07.2009
comment
@Mihail Я опубликовал свой собственный тест во втором ответе ниже. - person ; 13.07.2009

Мне нравится следовать правилу «говори то, что имеешь в виду».

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

person Jason Creighton    schedule 12.07.2009
comment
Бьюсь об заклад, что если бы язык был назван ++ C, люди делали бы это по умолчанию гораздо чаще. - person Reunanen; 13.07.2009
comment
Возможно, он называется C ++, потому что он по-прежнему действует как C при определенном использовании. - person Tom Leys; 13.07.2009
comment
когда вы видите i ++ в коде, очевидно, что поведение постинкремента действительно было задумано. Вы, должно быть, один из немногих, кто помнит разницу ;-) - person quant_dev; 13.07.2009

Несколько моментов:

  • Во-первых, вы вряд ли заметите существенную разницу в производительности каким-либо образом.
  • Во-вторых, ваш бенчмаркинг бесполезен, если у вас отключена оптимизация. Мы хотим знать, дает ли это изменение более или менее эффективный код, а это означает, что мы должны использовать его с наиболее эффективным кодом, который может создать компилятор. Нас не волнует, быстрее ли он в неоптимизированных сборках, нам нужно знать, быстрее ли он в оптимизированных.
  • Для встроенных типов данных, таких как целые числа, компилятор обычно может оптимизировать разницу. Проблема в основном возникает для более сложных типов с перегруженными итераторами приращения, когда компилятор не может тривиально увидеть, что две операции будут эквивалентны в контексте.
  • Вы должны использовать код, который наиболее четко выражает ваше намерение. Вы хотите «прибавить единицу к значению» или «прибавить единицу к значению, но продолжить работу над исходным значением еще немного»? Обычно так и бывает в первом случае, и тогда предварительное приращение лучше выражает ваше намерение.

Если вы хотите показать разницу, самый простой вариант - просто задействовать оба оператора и указать, что для одного требуется дополнительная копия, а для другого - нет.

person jalf    schedule 12.07.2009

Этот код и комментарии к нему должны демонстрировать различия между ними.

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

Вы можете видеть, что у постфикса есть дополнительный шаг (шаг 1), который включает создание копии объекта. Это влияет как на потребление памяти, так и на время выполнения. Вот почему префикс более эффективен, чем постфикс для неосновных типов.

В зависимости от some_ridiculously_big_type, а также от того, что вы делаете с результатом инкремента, вы сможете увидеть разницу с оптимизацией или без нее.

person Nathan Fellman    schedule 12.07.2009

В ответ Михаилу это несколько более переносимая версия его кода:

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

Внешние петли позволяют мне менять тайминги, чтобы получить что-то подходящее для моей платформы.

Я больше не использую VC ++, поэтому я скомпилировал его (в Windows) с помощью:

g++ -O3 t.cpp

Затем я запустил его, чередуя:

a.exe   

и

a.exe 1

Мои результаты по времени были примерно одинаковыми для обоих случаев. Иногда одна версия будет быстрее на 20%, а иногда другая. Я предполагаю, что это связано с другими процессами, запущенными в моей системе.

person Community    schedule 12.07.2009
comment
Таким образом, похоже, что он ведет себя совершенно по-разному при компиляции с разными инструментами и запуске на разных платформах. - person Mikhail Churbanov; 13.07.2009
comment
Пожалуйста, попробуйте мой код с вашим компилятором, прежде чем вы сделаете это предположение. - person ; 13.07.2009
comment
10-11 против 13. Чем сложнее программа, тем меньше разница в ++ вы увидите. - person Mikhail Churbanov; 13.07.2009

Попробуйте использовать 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 с использованием / O2 или / Ox, проверено на моем настольном компьютере и на ноутбуке.

Стабильно что-то обходится на ноуте, на десктопе номера немного другие (но скорость примерно такая же):

i++ > 8xx < 
++i > 6xx <

xx означает, что числа разные, например 813 против 640 - все еще примерно на 20% ускорение.

И еще один момент - если вы замените «d + =» на «d =», вы увидите приятный трюк оптимизации:

i++ > 935 <
++i > 0 <

Однако это довольно специфично. Но ведь я не вижу причин менять свое мнение и думать, что разницы нет :)

person Mikhail Churbanov    schedule 12.07.2009
comment
Пока i и d являются целыми числами, вы не должны видеть здесь никакой разницы. - person Nathan Fellman; 13.07.2009
comment
Однако в VS2005 с параметром / O2 я действительно вижу разницу - ++ i быстрее. Но мне просто любопытно, почему в отладочной версии - i ++ дает мне лучшую производительность? - person Mikhail Churbanov; 13.07.2009
comment
См. Мой второй ответ ниже для моих результатов. - person ; 13.07.2009
comment
-1 Вы вычисляете разные вещи: результат d в первом и втором цикле отличается значением SOME_BIG_CONSTANT, потому что сначала вы добавляете перед увеличением, а во втором - после. На самом деле это приводит к другому коду. Вам следует попробовать: d + = i; я ++; или d + = i; ++ i; чтобы увидеть разницу (и вы не найдете) - person Artyom; 13.07.2009
comment
Также я бы посоветовал разместить на распечатке размеры GetTickCount увеличенного размера. - person Artyom; 13.07.2009
comment
Спасибо, исправил код - теперь вычисляю те же значения. А printf действительно лучше разместить снаружи, чтобы тест был чище. - person Mikhail Churbanov; 13.07.2009
comment
Вы по-прежнему делаете разные вещи, даже если рассчитываете те же значения. - person Artyom; 13.07.2009
comment
Тем не менее, вы можете привести мне пример, в котором два разных оператора могут делать абсолютно одно и то же. Вопрос - действительно ли эта разница так важна? - person Mikhail Churbanov; 13.07.2009
comment
Идея состоит не в том, чтобы выполнять одну и ту же работу, а в том, чтобы выполнять тот же объем работы. - person Mikhail Churbanov; 13.07.2009

Может быть, вы могли бы просто показать теоретическую разницу, выписав обе версии с инструкциями по сборке x86? Как многие люди указывали ранее, компилятор всегда принимает собственное решение о том, как лучше всего скомпилировать / собрать программу.

Если пример предназначен для студентов, не знакомых с набором инструкций x86, вы можете рассмотреть возможность использования набора инструкций MIPS32 - по какой-то странной причине многим кажется, что его легче понять, чем сборку x86.

person Community    schedule 13.07.2009

Хорошо, вся эта префиксная / постфиксная "оптимизация" - это просто ... какое-то большое недоразумение.

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

Это может быть правильным для некоторых неэффективных реализаций итераторов. Однако в 99% случаев даже с итераторами STL нет никакой разницы, потому что компилятор знает, как его оптимизировать, а фактические итераторы - это просто указатели, которые выглядят как класс. И, конечно же, нет никакой разницы для примитивных типов, таких как целые числа в указателях.

Так что ... забудь об этом.

РЕДАКТИРОВАТЬ: Очистка

Как я уже упоминал, большинство классов итераторов STL - это просто указатели, обернутые классами, все функции-члены которых встроены, что позволяет не оптимизировать такие нерелевантная копия.

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

В качестве небольшого доказательства возьмите этот код:

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.

person Artyom    schedule 12.07.2009
comment
Есть множество случаев, когда итераторы являются не просто указателями, и когда у компилятора мало шансов его оптимизировать. - person jalf; 13.07.2009
comment
Это кажется достаточно важным, чтобы указать на это в любом другом учебнике. Я ищу наглядный пример, чтобы продемонстрировать эту концепцию кому-то новичку в программировании. Так что не говори мне забыть об этом. - person cschol; 13.07.2009
comment
Я не понимаю: основная идея, что i ++ возвращает свою исходную копию и, следовательно, требует копирования значения. Это может быть правильным для некоторых неэффективных реализаций итераторов. Это правильно ... всегда. i++ должен вернуть исходное значение. Называть что-то неэффективным из-за того, что оно соответствует стандарту, кажется немного странным. - person GManNickG; 13.07.2009
comment
В общем, учитывая, что ++ i будет быстрее, чем i ++, компилятор может преобразовать op (i ++) в op (i); ++ i ', как вы указываете. Теперь дело в том, что всякий раз, когда итератор не является базовым типом (указателем) и имеет операторы приращения, предоставленные пользователем, компилятор не может выполнить эту оптимизацию, поскольку он не может знать о возможных побочных эффектах (включая, помимо прочего, операцию постинкремента, генерирующую исключение и таким образом, никогда не выполняется 'op (i)'). Теперь с точки зрения производительности разница может быть незначительной и не стоит пересматривать код, используя предварительное приращение вместо пост-приращения, поскольку правило стиля подходит. - person David Rodríguez - dribeas; 13.07.2009