2017-02-01 1 views
2

Рассмотрим C код:Как измерить скорость короткой части кода C/сборки?

#include <complex.h> 
complex float f(complex float x[]) { 
    complex float p = 1.0; 
    for (int i = 0; i < 32; i++) 
    p += x[i]; 
    return p; 
} 

Процессор Intel C Compiler работать с -O3 -march=core-avx2 дает:

f: 
     vmovups ymm1, YMMWORD PTR [rdi]      #5.10 
     vmovups ymm2, YMMWORD PTR [64+rdi]     #5.10 
     vmovups ymm5, YMMWORD PTR [128+rdi]     #5.10 
     vmovups ymm6, YMMWORD PTR [192+rdi]     #5.10 
     vmovsd xmm0, QWORD PTR p.152.0.0.1[rip]    #3.19 
     vaddps ymm3, ymm1, YMMWORD PTR [32+rdi]    #3.19 
     vaddps ymm4, ymm2, YMMWORD PTR [96+rdi]    #3.19 
     vaddps ymm7, ymm5, YMMWORD PTR [160+rdi]    #3.19 
     vaddps ymm8, ymm6, YMMWORD PTR [224+rdi]    #3.19 
     vaddps ymm9, ymm3, ymm4        #3.19 
     vaddps ymm10, ymm7, ymm8        #3.19 
     vaddps ymm11, ymm9, ymm10       #3.19 
     vextractf128 xmm12, ymm11, 1       #3.19 
     vaddps xmm13, xmm11, xmm12       #3.19 
     vmovhlps xmm14, xmm13, xmm13       #3.19 
     vaddps xmm15, xmm13, xmm14       #3.19 
     vaddps xmm0, xmm15, xmm0        #3.19 
     vzeroupper            #6.10 
     ret              #6.10 

GCC версии 7 (снимок) с -O3 -march=core-avx2 -ffast-math дает:

f: 
     lea  r10, [rsp+8] 
     and  rsp, -32 
     push QWORD PTR [r10-8] 
     push rbp 
     mov  rbp, rsp 
     push r10 
     vmovups ymm0, YMMWORD PTR [rdi+64] 
     vmovaps ymm1, YMMWORD PTR .LC0[rip] 
     vaddps ymm0, ymm0, YMMWORD PTR [rdi+32] 
     vaddps ymm1, ymm1, YMMWORD PTR [rdi] 
     vaddps ymm0, ymm0, ymm1 
     vmovups ymm1, YMMWORD PTR [rdi+128] 
     vaddps ymm1, ymm1, YMMWORD PTR [rdi+96] 
     vaddps ymm0, ymm0, ymm1 
     vmovups ymm1, YMMWORD PTR [rdi+192] 
     vaddps ymm1, ymm1, YMMWORD PTR [rdi+160] 
     vaddps ymm0, ymm0, ymm1 
     vaddps ymm0, ymm0, YMMWORD PTR [rdi+224] 
     vunpckhps  xmm3, xmm0, xmm0 
     vshufps xmm2, xmm0, xmm0, 255 
     vshufps xmm1, xmm0, xmm0, 85 
     vaddss xmm1, xmm2, xmm1 
     vaddss xmm3, xmm3, xmm0 
     vextractf128 xmm0, ymm0, 0x1 
     vunpckhps  xmm4, xmm0, xmm0 
     vshufps xmm2, xmm0, xmm0, 85 
     vaddss xmm4, xmm4, xmm0 
     vshufps xmm0, xmm0, xmm0, 255 
     vaddss xmm0, xmm2, xmm0 
     vaddss xmm3, xmm3, xmm4 
     vaddss xmm1, xmm1, xmm0 
     vmovss DWORD PTR [rbp-24], xmm3 
     vmovss DWORD PTR [rbp-20], xmm1 
     vzeroupper 
     vmovq xmm0, QWORD PTR [rbp-24] 
     pop  r10 
     pop  rbp 
     lea  rsp, [r10-8] 
     ret 

Я заинтересован в котором один быстрее, поэтому было бы здорово измерить время работы.

Однако я не знаю, как измерить время работы кода, которое занимает так мало времени.

Какой код выполняется быстрее и как можно измерить его надежно?

+0

Вы можете использовать эту библиотеку: https://github.com/google/benchmark –

ответ

4

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

Это обеспечит время выполнения до нетривиального уровня и усложнит любые различия, вызванные планированием ОС.

void test_f() 
{ 
    complex float x[32] = { 1+2i, 2+3i }; // add as many as needed. 
              // here i is a special 
              // constant for complex numbers 
    int i; 
    for (i=0; i<10000000; i++) { 
     f(x); 
    } 
} 
+0

не будет ли оптимизатор устранить цикл? – eleanora

+1

Для gcc вы можете использовать '__attribute__ ((optimize (0)))' в тестовой функции, чтобы предотвратить ее оптимизацию. В качестве альтернативы вы помещаете тестовую функцию в другой файл .c, скомпилированный с различными настройками оптимизации. – dbush

+0

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

1

Повторите это достаточно часто, чтобы потребовалось достаточно много времени. Слишком короткий делает его более уязвимым к незначительным временным искажениям (накладные расходы на время измерения вообще, инструкции, перескакивающие из инструкции по чтению времени в или из области с эффективной синхронизацией благодаря OoOE, независимо) слишком долго. действительно важно, если вы не пурист. Обычно вы можете догадаться, каково должно быть фактическое время, измеренное время будет немного выше из-за прерываний и таких, но вы должны получить результат, «близкий» к разумному значению после того, как вы настроитесь на тактовую частоту (для измерений латентности вы должны например, получить целое число циклов). Сделайте несколько прогонов и зарисуйте их, игнорируйте странные выбросы, особенно наверху.

Убедитесь, что вы находитесь в режиме турбонаддува (или отключите все частотное масштабирование в настройках BIOS) и что векторные единицы «бодрствуют» (для кода AVX) перед синхронизацией, так что сделайте несколько разминок. Тот же код, который вы намереваетесь вовремя, может это сделать.

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

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

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

FWIW ICM asm выглядит быстрее, GCC делает много скалярной математики.

+0

Как избежать оптимизатора, исключающего цикл, если вы просто повторите его? – eleanora

+1

@eleanora вы можете поместить функцию в другую единицу компиляции (а затем не использовать LTO) (тогда ее нужно вызывать в цикле, поскольку оптимизатор должен предположить, что функция может сделать что-то смешное) или просто написать цикл в сборке и быть уверенным, что он делает именно то, что вы хотите – harold

+0

Можете ли вы показать код, необходимый для этих идей? Если можно добавить цикл в сборку, который выглядит как лучшее решение, но я не знаю, как смешивать C и сборку. – eleanora

1

Есть несколько шагов, которые вы должны сделать, когда вы хотите, чтобы выполнить измерение производительности:

  1. Повторите операцию измерить достаточно часто, чтобы получить до времени, что вы можете измерить точно. Обычно достаточно одного-десяти секунд. Это может быть достигнуто простым циклом практически во всех случаях.

  2. Предотвратите оптимизатор от оптимизации повторений или оптимизации измеренной операции полностью из-за ее неиспользуемых результатов.

    Есть несколько возможных подходов к этому:

    • Изменение входа в каждой итерации и на самом деле использовать все результаты. В вашем случае, это может выглядеть примерно так:

      complex float accu = 0+0i; 
      for(int i = 0; i < 100000000; i++) { 
          x[i%32] += 42+3i; //different input on each pass 
          accu += f(x); 
      } 
      printComplex(accu); //this depends on the output of all passes 
      

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

    • Разделить на независимые единицы компиляции: Поместить цикл повторения в один .c файл и поместить функцию для тестирования в другой .c файл. Компилируйте как отдельно с полной оптимизацией, так и ссылку без оптимизации.

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

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

    • Используйте некоторые директивы компилятора, такие как #pragma или __attribute__(()), чтобы избежать вредных оптимизаций. Я никогда не использовал это, поскольку любой из двух подходов выше хорошо служил мне во всех случаях, но это, безусловно, вариант. Однако детали являются специфическими для компилятора.

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

    complex float f(complex float x[]) { 
        complex float p = 1.0; 
        return p; 
    } 
    
+0

Как скомпилировать с полной оптимизацией, но ссылку без оптимизации? – eleanora

+0

Вы просто используете 'gcc -O3' при компиляции и используете' gcc' при связывании. Обычно компоновщик просто обрабатывает символы, созданные во время компиляции в виде черных ящиков. Однако, afaik, есть некоторые оптимизации времени соединения, которые могут быть включены явно. Просто не указывайте их в команде компоновщика, и все должно быть в порядке. – cmaster

Смежные вопросы