Трудности измерения производительности C/С++

Я написал фрагмент кода C, чтобы показать точку в обсуждении оптимизаций и прогнозирования ветвлений. Затем я заметил еще более разнообразный результат, чем ожидал. Моя цель состояла в том, чтобы написать его на языке, который является общим подмножеством между С++ и C, стандартным для обоих языков, и это довольно портативно. Он был протестирован на разных ПК с ОС Windows:

#include <stdio.h>
#include <time.h>

/// @return - time difference between start and stop in milliseconds
int ms_elapsed( clock_t start, clock_t stop )
{
    return (int)( 1000.0 * ( stop - start ) / CLOCKS_PER_SEC );
}

int const Billion = 1000000000;
/// & with numbers up to Billion gives 0, 0, 2, 2 repeating pattern 
int const Pattern_0_0_2_2 = 0x40000002; 

/// @return - half of Billion  
int unpredictableIfs()
{
    int sum = 0;
    for ( int i = 0; i < Billion; ++i )
    {
        // true, true, false, false ...
        if ( ( i & Pattern_0_0_2_2 ) == 0 )
        {
            ++sum;
        }
    }
    return sum;
}

/// @return - half of Billion  
int noIfs()
{
    int sum = 0;
    for ( int i = 0; i < Billion; ++i )
    {
        // 1, 1, 0, 0 ...
        sum += ( i & Pattern_0_0_2_2 ) == 0;
    }
    return sum;
}

int main()
{
    clock_t volatile start;
    clock_t volatile stop;
    int volatile sum;
    printf( "Puzzling measurements:\n" );

    start = clock();
    sum = unpredictableIfs();
    stop = clock();
    printf( "Unpredictable ifs took %d msec; answer was %d\n"
          , ms_elapsed(start, stop), sum );

    start = clock();
    sum = unpredictableIfs();
    stop = clock();
    printf( "Unpredictable ifs took %d msec; answer was %d\n"
          , ms_elapsed(start, stop), sum );

    start = clock();
    sum = noIfs();
    stop = clock();
    printf( "Same without ifs took %d msec; answer was %d\n"
          , ms_elapsed(start, stop), sum );

    start = clock();
    sum = unpredictableIfs();
    stop = clock();
    printf( "Unpredictable ifs took %d msec; answer was %d\n"
          , ms_elapsed(start, stop), sum );
}

Скомпилирован с VS2010;/O2. Результаты Intel Core 2, WinXP:

Puzzling measurements:
Unpredictable ifs took 1344 msec; answer was 500000000
Unpredictable ifs took 1016 msec; answer was 500000000
Same without ifs took 1031 msec; answer was 500000000
Unpredictable ifs took 4797 msec; answer was 500000000

Изменить: Полные ключи компилятора:

/Zi/nologo/W3/WX-/O2/Oi/Oy-/GL/D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm -/EHsc/GS/Gy/fp: exact/Zc: wchar_t/Zc: forScope/Fp "Release\Trying.pch" /Fa "Release\" /Fo "Release\" /Fd "Release\vc100.pdb" /Gd/analysis -/errorReport: queue

Другое лицо опубликовано такое... Скомпилировано с MinGW, g++ 4.71, -O1 оптимизация Результаты Intel Core 2, WinXP:

Puzzling measurements:
Unpredictable ifs took 1656 msec; answer was 500000000
Unpredictable ifs took 0 msec; answer was 500000000
Same without ifs took 1969 msec; answer was 500000000
Unpredictable ifs took 0 msec; answer was 500000000

Также он опубликовал такие результаты для оптимизации -O3:

Puzzling measurements:
Unpredictable ifs took 1890 msec; answer was 500000000
Unpredictable ifs took 2516 msec; answer was 500000000
Same without ifs took 1422 msec; answer was 500000000
Unpredictable ifs took 2516 msec; answer was 500000000

Теперь у меня вопрос. Что здесь происходит?

Подробнее... Как фиксированная функция может занимать столько времени? Что-то не так в моем коде? Что-то сложно с процессором Intel? Компиляторы делают что-то странное? Может ли это быть из-за 32-разрядного кода, запущенного на 64-битном процессоре?

Спасибо за внимание!

Edit: Я принимаю, что g++ -O1 просто повторно использует возвращаемые значения в двух других вызовах. Я также согласен с тем, что g++ -O2 и g++ -O3 имеют дефект, который оставляет оптимизацию. Значительное разнообразие измеренных скоростей (450%!!!) кажется загадочным.

Я посмотрел на разборку кода, созданного VS2010. Он сделал inline unpredictableIfs 3 раза. Встроенный код был довольно схожим; петля была такой же. Он не встроил noIfs. Он немного отбросил noIfs. Для одной итерации требуется 4 шага. noIfs вычислить, как было написано, а unpredictableIfs использовать jne для перехода через приращение.

Ответы

Ответ 1

С -O1 gcc-4.7.1 вызывает unpredictableIfs только один раз и повторяет результат, так как он распознает, что это чистая функция, поэтому результат будет одинаковым при каждом вызове. (Шахта была проверена, посмотрев на сгенерированную сборку.)

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

Кроме того, мой gcc-4.7.1 лучше всего работает с unpredictableIfs при использовании -O1 или -O2 (кроме проблемы повторного использования, оба производят один и тот же код), а noIfs обрабатывается много лучше с -O3. Временные интервалы между разными прогонами одного и того же кода, однако, здесь непротиворечивы - равны или различаются на 10 миллисекунд (зернистость clock), поэтому я понятия не имею, что может вызвать существенно разные времена для unpredictableIfs, о которых вы сообщали для -O3.

С -O2 цикл для unpredictableIfs идентичен коду, сгенерированному с помощью -O1 (за исключением замены регистров):

.L12:
    movl    %eax, %ecx
    andl    $1073741826, %ecx
    cmpl    $1, %ecx
    adcl    $0, %edx
    addl    $1, %eax
    cmpl    $1000000000, %eax
    jne .L12

и для noIfs он похож:

.L15:
    xorl    %ecx, %ecx
    testl   $1073741826, %eax
    sete    %cl
    addl    $1, %eax
    addl    %ecx, %edx
    cmpl    $1000000000, %eax
    jne .L15

где он был

.L7:
    testl   $1073741826, %edx
    sete    %cl
    movzbl  %cl, %ecx
    addl    %ecx, %eax
    addl    $1, %edx
    cmpl    $1000000000, %edx
    jne .L7

с -O1. Оба цикла выполняются в одно и то же время, причем unpredictableIfs бит быстрее.

С -O3 цикл для unpredictableIfs ухудшается,

.L14:
    leal    1(%rdx), %ecx
    testl   $1073741826, %eax
    cmove   %ecx, %edx
    addl    $1, %eax
    cmpl    $1000000000, %eax
    jne     .L14

и для noIfs (включая здесь установочный код), он становится лучше:

    pxor    %xmm2, %xmm2
    movq    %rax, 32(%rsp)
    movdqa  .LC3(%rip), %xmm6
    xorl    %eax, %eax
    movdqa  .LC2(%rip), %xmm1
    movdqa  %xmm2, %xmm3
    movdqa  .LC4(%rip), %xmm5
    movdqa  .LC5(%rip), %xmm4
    .p2align 4,,10
    .p2align 3
.L18:
    movdqa  %xmm1, %xmm0
    addl    $1, %eax
    paddd   %xmm6, %xmm1
    cmpl    $250000000, %eax
    pand    %xmm5, %xmm0
    pcmpeqd %xmm3, %xmm0
    pand    %xmm4, %xmm0
    paddd   %xmm0, %xmm2
    jne .L18

.LC2:
    .long   0
    .long   1
    .long   2
    .long   3
    .align 16
.LC3:
    .long   4
    .long   4
    .long   4
    .long   4
    .align 16
.LC4:
    .long   1073741826
    .long   1073741826
    .long   1073741826
    .long   1073741826
    .align 16
.LC5:
    .long   1
    .long   1
    .long   1
    .long   1

он вычисляет сразу четыре итерации, и, соответственно, noIfs выполняется почти в четыре раза быстрее.

Ответ 2

Правильно, глядя на код ассемблера из gcc в 64-разрядной Linux, в первом случае с -O1 функция UnpredictableIfs действительно вызывается только один раз, а результат повторно используется.

С -O2 и -O3 функции встроены, и время, которое требуется, должно быть идентичным. Также нет никаких ветвей в любом бите кода, но перевод двух битов кода несколько отличается, я отключил строки, которые обновляют "сумму" [в %edx в обоих случаях]

UnpredictableIfs:

movl    %eax, %ecx
andl    $1073741826, %ecx
cmpl    $1, %ecx
adcl    $0, %edx
addl    $1, %eax

NoIfs:

xorl    %ecx, %ecx
testl   $1073741826, %eax
sete    %cl
addl    $1, %eax
addl    %ecx, %edx

Как вы можете видеть, это не совсем идентично, но это очень похоже на вещи.

Ответ 3

Относительно диапазона результатов в Windows (от 1016 мс до 4797 мс): вы должны знать, что clock() в MSVC возвращает прошедшее время стены. В стандарте указано, что clock() должен возвращать приблизительное время процессора, затрачиваемое процессом, а другие реализации лучше справляются с этим.

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

Также обратите внимание, что clock() на многих ПК с ОС Windows имеет довольно паршивое разрешение, часто как 11-19 мс. Вы сделали достаточно итераций, что только около 1%, поэтому я не думаю, что это часть несоответствия, но хорошо знать, когда вы пытаетесь написать бенчмарк. Я понимаю, что вы идете на переносимость, но если вам нужны лучшие измерения в Windows, вы можете использовать QueryPerformanceCounter, что почти наверняка даст вам гораздо лучшее разрешение, хотя оно все еще только истекло, настенное время.

ОБНОВЛЕНИЕ: После того, как я узнал, что длительное время выполнения в одном случае происходит последовательно, я запускал VS2010 и воспроизводил результаты. Обычно я получаю что-то около 1000 мс для некоторых прогонов, 750 мс для других и 5000+ мс для необъяснимых.

замечания:

  • Во всех случаях код incredictableIfs() был встроен.
  • Удаление кода noIfs() не повлияло (поэтому долгое время не было побочным эффектом этого кода).
  • Настройка привязки потоков к одному процессору не имела эффекта.
  • Времена 5000 мс неизменно были более поздними. Я отметил, что более поздние экземпляры имели дополнительную инструкцию перед началом цикла: lea ecx,[ecx]. Я не понимаю, почему это должно сделать разницу в 5 раз. Кроме того, ранние и более поздние экземпляры были идентичными.
  • Удаление volatile из переменных start и stop дало меньше длинных прогонов, больше пробега 750 мс и не работает 1000 мс. (Созданный код цикла во всех случаях выглядит точно таким же, а не lea s.)
  • Удаляя volatile из переменной sum (но сохраняя ее для таймеров синхронизации), длинные прогоны могут выполняться в любой позиции.
  • Если вы удалите все квалификаторы volatile, вы получите последовательные, быстрые (750 мс) прогоны. (Код выглядит идентичным предыдущим, но edi был выбран для sum вместо ecx.)

Я не уверен, что заключить из всего этого, за исключением того, что volatile имеет непредсказуемые последствия производительности с MSVC, поэтому вы должны применять его только при необходимости.

ОБНОВЛЕНИЕ 2: Я вижу согласованные различия во времени выполнения, связанные с использованием летучих, даже несмотря на то, что разборка почти идентична.

С изменчивым:

Puzzling measurements:
Unpredictable ifs took 643 msec; answer was 500000000
Unpredictable ifs took 1248 msec; answer was 500000000
Unpredictable ifs took 605 msec; answer was 500000000
Unpredictable ifs took 4611 msec; answer was 500000000
Unpredictable ifs took 4706 msec; answer was 500000000
Unpredictable ifs took 4516 msec; answer was 500000000
Unpredictable ifs took 4382 msec; answer was 500000000

Разбор для каждого экземпляра выглядит следующим образом:

    start = clock();
010D1015  mov         esi,dword ptr [__imp__clock (10D20A0h)]  
010D101B  add         esp,4  
010D101E  call        esi  
010D1020  mov         dword ptr [start],eax  
    sum = unpredictableIfs();
010D1023  xor         ecx,ecx  
010D1025  xor         eax,eax  
010D1027  test        eax,40000002h  
010D102C  jne         main+2Fh (10D102Fh)  
010D102E  inc         ecx  
010D102F  inc         eax  
010D1030  cmp         eax,3B9ACA00h  
010D1035  jl          main+27h (10D1027h)  
010D1037  mov         dword ptr [sum],ecx  
    stop = clock();
010D103A  call        esi  
010D103C  mov         dword ptr [stop],eax  

Без изменчивости:

Puzzling measurements:
Unpredictable ifs took 644 msec; answer was 500000000
Unpredictable ifs took 624 msec; answer was 500000000
Unpredictable ifs took 624 msec; answer was 500000000
Unpredictable ifs took 605 msec; answer was 500000000
Unpredictable ifs took 599 msec; answer was 500000000
Unpredictable ifs took 599 msec; answer was 500000000
Unpredictable ifs took 599 msec; answer was 500000000

    start = clock();
00321014  mov         esi,dword ptr [__imp__clock (3220A0h)]  
0032101A  add         esp,4  
0032101D  call        esi  
0032101F  mov         dword ptr [start],eax  
    sum = unpredictableIfs();
00321022  xor         ebx,ebx  
00321024  xor         eax,eax  
00321026  test        eax,40000002h  
0032102B  jne         main+2Eh (32102Eh)  
0032102D  inc         ebx  
0032102E  inc         eax  
0032102F  cmp         eax,3B9ACA00h  
00321034  jl          main+26h (321026h)  
    stop = clock();
00321036  call        esi
// The only optimization I see is here, where eax isn't explicitly stored
// in stop but is instead immediately used to compute the value for the
// printf that follows.

Помимо выбора регистра, я не вижу существенной разницы.