в то время как (n> 1) на 25% быстрее, чем while (n)? - PullRequest
0 голосов
/ 17 сентября 2011

У меня есть две логически эквивалентные функции:

long ipow1(int base, int exp) {
    // HISTORICAL NOTE:
    // This wasn't here in the original question, I edited it in,
    if (exp == 0) return 1;

    long result = 1;

    while (exp > 1) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result * base;
}

long ipow2(int base, int exp) { 
    long result = 1;

    while (exp) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result;
}

ВНИМАНИЕ:

Эти циклы эквивалентны , потому что в первомcase мы возвращаем result * base (обрабатывает случай, когда exp уменьшается или был уменьшен до 1), но во втором случае мы возвращаем result.


Как ни странно, обас -O3 и -O0 ipow1, следовательно, превосходит ipow2 примерно на 25%.Как это возможно?

Я на Windows 7, x64, gcc 4.5.2 и компилирую с gcc ipow.c -O0 -std=c99.

И это мой код профилирования:

int main(int argc, char *argv[]) {
    LARGE_INTEGER ticksPerSecond;
    LARGE_INTEGER tick;
    LARGE_INTEGER start_ticks, end_ticks, cputime;

    double totaltime = 0;
    int repetitions = 10000;
    int rep = 0;
    int nopti = 0;

    for (rep = 0; rep < repetitions; rep++) {
        if (!QueryPerformanceFrequency(&ticksPerSecond)) printf("\tno go QueryPerformance not present");
        if (!QueryPerformanceCounter(&tick)) printf("no go counter not installed");  
        QueryPerformanceCounter(&start_ticks); 

        /* start real code */

        for (int i = 0; i < 55; i++) {
            for (int j = 0; j < 11; j++) {
                nopti = ipow1(i, j); // or ipow2
            }
        }

        /* end code */

        QueryPerformanceCounter(&end_ticks); 
        cputime.QuadPart = end_ticks.QuadPart - start_ticks.QuadPart;
        totaltime += (double)cputime.QuadPart / (double)ticksPerSecond.QuadPart;
    }   

    printf("\tTotal elapsed CPU time:   %.9f  sec  with %d repetitions - %ld:\n", totaltime, repetitions, nopti);

    return 0;
}

Ответы [ 5 ]

10 голосов
/ 17 сентября 2011

Нет, действительно, эти два НЕ эквивалентны. ipow2 возвращает правильные результаты, когда ipow1 нет.

P.S. Мне все равно, сколько комментариев вы оставляете, «объясняя», почему они одинаковые, для опровержения ваших утверждений требуется всего один контрпример.

P.P.S. -1 на вопрос о вашем невыносимом высокомерии по отношению ко всем, кто уже пытался указать вам на это.

3 голосов
/ 17 сентября 2011

Так как while (exp> 1) for будет работать с exp до 2 (он будет выполняться с exp = 2, уменьшать его до 1 и затем завершать цикл). С помощью while (exp), for будет работать с exp до 1 (он будет выполняться с exp = 1, уменьшать его до 0 и затем завершать цикл).

Так что с while (exp) у вас есть дополнительная итерация, которая требует дополнительного времени для запуска.

РЕДАКТИРОВАТЬ : Даже с умножением после цикла с exp> 1, помните, что умножение не единственное в цикле.

2 голосов
/ 17 сентября 2011

Если вы не хотите читать весь этот пропуск, я получу разницу в 21% только из-за анализа кода.

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

long ipow1(int base, int exp) {
    long result = 1;

    while (exp > 1) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result * base;
}

long ipow2(int base, int exp) {
    long result = 1;

    while (exp) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result;
}

0000000000000000 <ipow1>:
   0:   83 fe 01                cmp    $0x1,%esi
   3:   ba 01 00 00 00          mov    $0x1,%edx
   8:   7e 1d                   jle    27 <ipow1+0x27>
   a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
  10:   40 f6 c6 01             test   $0x1,%sil
  14:   74 07                   je     1d <ipow1+0x1d>
  16:   48 63 c7                movslq %edi,%rax
  19:   48 0f af d0             imul   %rax,%rdx
  1d:   d1 fe                   sar    %esi
  1f:   0f af ff                imul   %edi,%edi
  22:   83 fe 01                cmp    $0x1,%esi
  25:   7f e9                   jg     10 <ipow1+0x10>
  27:   48 63 c7                movslq %edi,%rax
  2a:   48 0f af c2             imul   %rdx,%rax
  2e:   c3                      retq   
  2f:   90                      nop

0000000000000030 <ipow2>:
  30:   85 f6                   test   %esi,%esi
  32:   b8 01 00 00 00          mov    $0x1,%eax
  37:   75 0a                   jne    43 <ipow2+0x13>
  39:   eb 19                   jmp    54 <ipow2+0x24>
  3b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  40:   0f af ff                imul   %edi,%edi
  43:   40 f6 c6 01             test   $0x1,%sil
  47:   74 07                   je     50 <ipow2+0x20>
  49:   48 63 d7                movslq %edi,%rdx
  4c:   48 0f af c2             imul   %rdx,%rax
  50:   d1 fe                   sar    %esi
  52:   75 ec                   jne    40 <ipow2+0x10>
  54:   f3 c3                   repz retq 

Изоляция петель:

    while (exp > 1) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }


//if exp & 1 not true jump to 1d to skip   
  10:   40 f6 c6 01             test   $0x1,%sil
  14:   74 07                   je     1d <ipow1+0x1d>
//result *= base  
  16:   48 63 c7                movslq %edi,%rax
  19:   48 0f af d0             imul   %rax,%rdx
//exp>>=1  
  1d:   d1 fe                   sar    %esi
//base *= base  
  1f:   0f af ff                imul   %edi,%edi
//while(exp>1) stayin the loop  
  22:   83 fe 01                cmp    $0x1,%esi
  25:   7f e9                   jg     10 <ipow1+0x10>

Сравнение чего-либо с нулем обычно спасает вас от инструкции, и вы можете увидеть это здесь

    while (exp) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }


//base *= base  
  40:   0f af ff                imul   %edi,%edi
//if exp & 1 not true jump to skip  
  43:   40 f6 c6 01             test   $0x1,%sil
  47:   74 07                   je     50 <ipow2+0x20>
//result *= base  
  49:   48 63 d7                movslq %edi,%rdx
  4c:   48 0f af c2             imul   %rdx,%rax
//exp>>=1  
  50:   d1 fe                   sar    %esi
//no need for a compare  
  52:   75 ec                   jne    40 <ipow2+0x10>

Ваш метод хронометража вызовет много ошибок / хаоса. В зависимости от частоты ударов петли и точности таймера вы можете создать большой выигрыш в одном и много потерь в другом. Этот метод обычно дает лучшую точность:

starttime = ... для (Rep = bignumber; представитель; rep--) { // тестируемый код ... } конечное время = ... итого = время окончания - время начала;

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

Также вы хотите использовать переменные переменные для ваших переменных таймера, помогая компилятору не перестраивать порядок выполнения. (там это видели).

Если мы посмотрим на это с точки зрения базовых множителей:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned int mults;

long ipow1(int base, int exp) {
    long result = 1;

    while (exp > 1) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
        mults++;
    }

    result *= base;

    return result;
}

long ipow2(int base, int exp) {
    long result = 1;

    while (exp) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
        mults++;
    }

    return result;
}


int main ( void )
{
    int i;
    int j;

    mults = 0;
        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow1(i, j); // or ipow2
            }
        }
    printf("mults %u\n",mults);

    mults=0;

        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow2(i, j); // or ipow2
            }
        }
    printf("mults %u\n",mults);

}

есть

mults 1045
mults 1595

50% больше для ipow2 (). На самом деле это не просто умножение, это то, что вы проходите цикл на 50% больше.

ipow1 () немного возвращает другие умножения:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned int mults;

long ipow1(int base, int exp) {
    long result = 1;

    while (exp > 1) {
        if (exp & 1) mults++;
        exp >>= 1;
        base *= base;
    }
    mults++;

    return result;
}

long ipow2(int base, int exp) {
    long result = 1;

    while (exp) {
        if (exp & 1) mults++;
        exp >>= 1;
        base *= base;
    }

    return result;
}


int main ( void )
{
    int i;
    int j;

    mults = 0;
        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow1(i, j); // or ipow2
            }
        }
    printf("mults %u\n",mults);

    mults=0;
        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow2(i, j); // or ipow2
            }
        }
    printf("mults %u\n",mults);

}

ipow1 () выполняет результат * = base разное (больше) раз, чем ipow2 ()

mults 990
mults 935

длинная * int может сделать их дороже. недостаточно, чтобы компенсировать потери вокруг цикла в ipow2 ().

Даже без разборки сделайте приблизительное предположение об операциях / инструкциях, которые, как вы надеетесь, использует компилятор. Если учесть, что процессоры здесь вообще не обязательно x86, то некоторые процессоры будут выполнять этот код лучше, чем другие (из ряда выполненных инструкций, не считая всех других факторов).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned int ops;

long ipow1(int base, int exp) {
    long result = 1;
    ops++; //result = immediate
    while (exp > 1) {
        ops++; // compare exp - 1
        ops++; // conditional jump
            //if (exp & 1)
        ops++; //exp&1
        ops++; //conditional jump
        if (exp & 1)
        {
            result *= base;
            ops++;
        }
        exp >>= 1;
        ops++;
        //ops+=?; //using a signed number can cost you this on some systems
        //always use unsigned unless you have a specific reason to use signed.
        //if this had been a short or char variable it might cost you even more
        //operations
        //if this needs to be signed it is what it is, just be aware of
        //the cost
        base *= base;
        ops++;
    }
    result *= base;
    ops++;
    return result;
}

long ipow2(int base, int exp) {
    long result = 1;
    ops++;
    while (exp) {
        //ops++; //cmp exp-0, often optimizes out;
        ops++; //conditional jump
        //if (exp & 1)
        ops++;
        ops++;
        if (exp & 1)
        {
            result *= base;
            ops++;
        }
        exp >>= 1;
        ops++;
        //ops+=?; //right shifting a signed number
        base *= base;
        ops++;
    }
    return result;
}



int main ( void )
{
    int i;
    int j;

    ops = 0;
        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow1(i, j); // or ipow2
            }
        }
    printf("ops %u\n",ops);

    ops=0;
        for (i = 0; i < 55; i++) {
            for (j = 0; j < 11; j++) {
                ipow2(i, j); // or ipow2
            }
        }
    printf("ops %u\n",ops);

}

Предполагая, что я посчитал все основные операции и не несправедливо дал одну функцию больше, чем другую:

ops 7865
ops 9515

ipow2 медленнее на 21% при использовании этого анализа.

Я думаю, что большой убийца проходит на 50% больше по циклу. Конечно, это зависит от данных, вы можете найти входные данные в тесте производительности, которые делают разницу между функциями больше или хуже, чем 25%, которые вы видите.

0 голосов
/ 17 сентября 2011

Это действительно генерирует тот же код сборки?Когда я попытался (с gcc 4.5.1 на OpenSuse 11.4, я признаю), я обнаружил небольшие различия.

ipow1.s:

cmpl    $1, -24(%rbp)
jg  .L4
movl    -20(%rbp), %eax
cltq
imulq   -8(%rbp), %rax
leave

ipow2.s:

cmpl    $0, -24(%rbp)
jne .L4
movq    -8(%rbp), %rax
leave

Возможно, предсказание ветвления процессора просто более эффективно с jg, чем с jne?Представляется маловероятным, что одна инструкция перехода будет выполняться на 25% быстрее, чем другая (особенно, когда cmpl выполнил большую часть тяжелой работы)

0 голосов
/ 17 сентября 2011

Ваши функции не «логически равны».

while (exp > 1){...}

равно НЕ логически равно

while (exp){...}

Почему вы говорите, что это так?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...