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

Web 2.0 Шоу является подкастом о появляющихся технологиях, обычно называемых "Web 2.0", и размещается Josh Owens и Adam Stacoviak.

15
задан Jens Erat 30 May 2013 в 08:39
поделиться

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 ", что устраняет обычную семантическую разницу.

23
ответ дан 1 December 2019 в 01:23
поделиться

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

8
ответ дан 1 December 2019 в 01:23
поделиться

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

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

0
ответ дан 1 December 2019 в 01:23
поделиться

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

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

0
ответ дан 1 December 2019 в 01:23
поделиться

Perhaps you could just show the theoretical difference by writing out both versions with x86 assembly instructions? As many people have pointed out before, compiler will always make its own decisions on how best to compile/assemble the program.

If the example is meant for students not familiar with the x86 instruction set, you might consider using the MIPS32 instruction set -- for some odd reason many people seem to find it to be easier to comprehend than x86 assembly.

0
ответ дан 1 December 2019 в 01:23
поделиться

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

Основная идея, что 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 .

-4
ответ дан 1 December 2019 в 01:23
поделиться

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

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

5
ответ дан 1 December 2019 в 01:23
поделиться

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

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

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

4
ответ дан 1 December 2019 в 01:23
поделиться

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

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), который включает создание копии объекта . Это имеет значение как для потребления памяти, так и для времени выполнения. Из-за префикс более эффективен, чем постфикс для неосновных типов.

0
ответ дан 1 December 2019 в 01:23
поделиться
Другие вопросы по тегам:

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