Как протестировать несколько строк программного кода C? - PullRequest
1 голос
/ 05 августа 2020

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

int square(int num) {
    int result = 0;
    if (num > 10) {
        result += num;
    }
    return result * result;
}

После удаления ветки if у меня есть следующее:

int square(int num) {
    int result = 0;
    int tmp = num > 10;
    result = result * tmp + num * tmp + result * !tmp;
    return result * result;
}

Теперь я хочу узнать, работает ли версия без веток быстрее. Я поискал и нашел инструмент под названием сверхтонкое (https://github.com/sharkdp/hyperfine). Итак, я написал следующую функцию main и протестировал две версии функции square с помощью hyperfine.

int main() {
    printf("%d\n", square(38));
    return 0;
}

Проблема в том, что на основе сверхтонкого результата я не могу определить, какой версия лучше. Как при программировании C люди обычно определяют, какая версия функции быстрее?

Ниже приведены некоторые из моих hyperfine результатов.

C:\my_projects\untitled>hyperfine branchless.exe
Benchmark #1: branchless.exe
  Time (mean ± σ):       5.4 ms ±   0.2 ms    [User: 2.2 ms, System: 3.2 ms]
  Range (min … max):     4.9 ms …   6.1 ms    230 runs

C:\my_projects\untitled>hyperfine branch.exe
Benchmark #1: branch.exe
  Time (mean ± σ):       6.1 ms ±   0.7 ms    [User: 2.2 ms, System: 3.7 ms]
  Range (min … max):     5.0 ms …   9.7 ms    225 runs

C:\my_projects\untitled>hyperfine branch.exe
Benchmark #1: branch.exe
  Time (mean ± σ):       5.5 ms ±   0.3 ms    [User: 2.1 ms, System: 3.5 ms]
  Range (min … max):     4.9 ms …   7.0 ms    211 runs

C:\my_projects\untitled>hyperfine branch.exe
Benchmark #1: branch.exe
  Time (mean ± σ):       5.6 ms ±   0.4 ms    [User: 2.0 ms, System: 3.9 ms]
  Range (min … max):     4.8 ms …   7.0 ms    217 runs

  Warning: Command took less than 5 ms to complete. Results might be inaccurate.


C:\my_projects\untitled>hyperfine branch.exe
Benchmark #1: branch.exe
  Time (mean ± σ):       5.7 ms ±   0.3 ms    [User: 1.9 ms, System: 4.0 ms]
  Range (min … max):     5.0 ms …   6.6 ms    220 runs

C:\my_projects\untitled>hyperfine branchless.exe
Benchmark #1: branchless.exe
  Time (mean ± σ):       5.6 ms ±   0.3 ms    [User: 1.9 ms, System: 3.9 ms]
  Range (min … max):     4.8 ms …   6.9 ms    219 runs

C:\my_projects\untitled>hyperfine branchless.exe
Benchmark #1: branchless.exe
  Time (mean ± σ):       5.8 ms ±   0.3 ms    [User: 1.5 ms, System: 4.0 ms]
  Range (min … max):     5.2 ms …   7.3 ms    224 runs

C:\my_projects\untitled>

Ответы [ 2 ]

3 голосов
/ 05 августа 2020

Как протестировать несколько строк C программного кода?

Скомпилировать код и проверить сгенерированную сборку вашим компилятором.

Обычно используется Godbolt и осмотрите там сгенерированную сборку. Godbolt link .

Полунезадежный способ - подсчитать выполненные инструкции сборки. Насчет windows не знаю - работаю на linux. С gdb я использую код, представленный в этом вопросе и с:

// 1.c
#if MACRO
int square(int num) {
    int result = 0;
    if (num > 10) {
        result += num;
    }
    return result * result;
}
#else
int square(int num) {
    int result = 0;
    int tmp = num > 10;
    result = result * tmp + num * tmp + result * !tmp;
    return result * result;
}
#endif
// start-stop places for counting assembly instructions
// Adding attribute and a specific asm syntax that is a GNU extension
// So that the compiler will not optimize the functions out
__attribute__((__noinline__)) void begin() { __asm__("nop"); }
__attribute__((__noinline__)) void finish() { __asm__("nop"); }
// trying to use volatile so that compiler 
// wouldn't optimize the function completely out
volatile int arg = 38, res;
int main() {
    begin();
    res = square(arg);
    finish();
}

Затем скомпилируйте и протестируйте в bash:

# a short function to count number of instructions executed between "begin" and "finish" functions
$ b() { printf "%s\n" 'set editing off' 'set prompt' 'set confirm off' 'set pagination off' 'b begin' 'r' 'set $count=0' 'while ($pc != finish)' 'stepi' 'set $count=$count+1' 'end' 'printf "The count of instruction between begin and finish is: %d\n", $count' 'q' | gdb "$1" |& grep 'The count'; }

# then compile and measure
$ gcc -D MACRO=0 1.c ; b a.out
The count of instruction between begin and finish is: 34
$ gcc -D MACRO=1 1.c ; b a.out
The count of instruction between begin and finish is: 22

Похоже, на моей платформе с компилятором gcc10 без каких-либо опций без оптимизаций вторая версия выполняет всего 12 инструкций. Но сравнивать вывод компилятора с оптимизацией не имеет смысла. После включения оптимизаций разница в одну инструкцию:

$ gcc -O -D MACRO=0 1.c ; b a.out
The count of instruction between begin and finish is: 11
$ gcc -O -D MACRO=1 1.c ; b a.out 
The count of instruction between begin and finish is: 10

Примечания:

  • с вашим кодом square(38) можно просто оптимизировать до no-op.
  • с вашим кодом и hyperfine branchless.exe вы сравниваете выполнение printf ie. время, необходимое для sh вывода и печати, а не выполнения square().
  • Как указано в этом ответе , вы можете использовать аппаратный счетчик, если он доступен *. 1037 *
1 голос
/ 05 августа 2020

Как сказано в примечании, время выполнения printf больше, чем время, которое вы хотите измерить, и независимо от этого время, которое вы пытаетесь измерить, слишком мало.

Чтобы иметь мера, которую вы должны поместить квадрат в файл и его вызов в другом файле в al oop, также без использования литералов, иначе сгенерированный код может быть прямым результатом и ничего более (никогда не недооценивайте сила оптимизации, которую компиляторы могут выполнять, когда они знают все, например, C ++ constexpr ).

Так, например:

file c1. c

int square(int num) {
    int result = 0;
    if (num > 10) {
        result += num;
    }
    return result * result;
}

файл c2. c

int square(int num) {
    int result = 0;
    int tmp = num > 10;
    result = result * tmp + num * tmp + result * !tmp;
    return result * result;
}

файл main. c

#include <stdio.h>

extern int square(int);

int main(int argc, char ** argv)
{
  int n, v, r = 0;
  
  if ((argc == 3) && 
      (sscanf(argv[1], "%d", &n) == 1) &&
      (sscanf(argv[2], "%d", &v) == 1))
    while (n--)
      r += square(v);
  return r;
}

Использование первого решения (без оптимизации):

/tmp % gcc c1.c main.c 
/tmp % time ./a.out 1000000000 38
2.315u 0.000s 0:02.41 95.8% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
2.316u 0.000s 0:02.41 95.8% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
2.316u 0.000s 0:02.41 95.8% 0+0k 0+0io 0pf+0w
/tmp %

Использование второго решения (без оптимизации):

/tmp % gcc c2.c main.c 
/tmp % time ./a.out 1000000000 38
3.087u 0.000s 0:03.21 95.9% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
3.107u 0.000s 0:03.23 95.9% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
3.098u 0.000s 0:03.22 95.9% 0+0k 0+0io 0pf+0w
/tmp  %

Таким образом, без оптимизации второе предложение требует больше времени, это все еще так, даже если разница между компиляцией с оптимизацией почти равна нулю:

/tmp % gcc -O2 c1.c main.c
/tmp % time ./a.out 1000000000 38
1.337u 0.000s 0:01.39 95.6% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
1.336u 0.001s 0:01.39 95.6% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
1.343u 0.000s 0:01.39 96.4% 0+0k 0+0io 0pf+0w
/tmp % 
/tmp % 
/tmp % gcc -O2 c2.c main.c
/tmp % time ./a.out 1000000000 38
1.341u 0.000s 0:01.39 96.4% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
1.343u 0.000s 0:01.40 95.7% 0+0k 0+0io 0pf+0w
/tmp % time ./a.out 1000000000 38
1.339u 0.000s 0:01.39 95.6% 0+0k 0+0io 0pf+0w
/tmp %

Я делал под Linux, но вы можете сделать то же самое под Windows, используя свой инструмент для измерения

Для информации, сгенерированный код с оптимизацией:

первый способ:

square:
.LFB0:
    .cfi_startproc
    movl    %edi, %edx
    xorl    %eax, %eax
    imull   %edi, %edx
    cmpl    $11, %edi
    cmovge  %edx, %eax
    ret

второй способ:

square:
.LFB0:
    .cfi_startproc
    xorl    %eax, %eax
    cmpl    $10, %edi
    setg    %al
    imull   %edi, %eax
    imull   %eax, %eax
    ret
    .cfi_endproc
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...