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), я должен увидеть разницу, чтобы показать концепцию. Что мне не хватает? Есть ли лучший пример, чтобы показать это?

+15
источник поделиться
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()

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

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

+23
источник

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

+8
источник
другие ответы

Связанные вопросы


Похожие вопросы

Мне нравится следовать правилу "скажи, что ты имеешь в виду".

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

+5
источник

Несколько точек:

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

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

+4
источник

Попробуйте использовать 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 <

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

0
источник

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

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, вы сможете увидеть разницу с оптимизацией или без нее.

0
источник

В ответ на 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%, а иногда и на другую. Это, я думаю, связано с другими процессами, запущенными в моей системе.

0
источник

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

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

0
источник

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

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

Это может быть правильным для некоторых неэффективных реализаций итераторов. Однако в 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.

-4
источник

Посмотрите другие вопросы по меткам или Задайте вопрос