如何轻松地对C代码进行基准测试?

71

有没有一个简单的库可以测试执行某段C代码所需的时间?我想要的是这样一种东西:

int main(){
    benchmarkBegin(0);
    //Do work
    double elapsedMS = benchmarkEnd(0);

    benchmarkBegin(1)
    //Do some more work
    double elapsedMS2 = benchmarkEnd(1);

    double speedup = benchmarkSpeedup(elapsedMS, elapsedMS2); //Calculates relative speedup
}

如果库能让你运行多次,对结果进行平均并计算时间方差,那将会很棒!


2
非常好的问题,这帮助了我很多。 - Nick Knowlson
1
在程序计时方面的替代方法:https://dev59.com/1Ws05IYBdhLWcg3wFOC8 - Ciro Santilli OurBigBook.com
1
大型封闭式Linux问题:https://dev59.com/n3RC5IYBdhLWcg3wOOL1 - Ciro Santilli OurBigBook.com
1
在同一程序中重复执行相似的工作可能会让编译器在它们之间进行优化。构建多个可执行文件,每个文件都微基准测试单个实现策略更安全(但更繁琐)。将整个程序的运行时间作为基准测试可以轻松比较perf stat的性能计数器结果,并且意味着您可以使用外部计时工具,例如time ./a.out,而无需在C中包含计时代码。尽管如此,在程序中使用计时代码可以避免计时初始化代码。并且一个程序的多个结果更简单。 - Peter Cordes
5个回答

80

使用在time.h中定义的函数clock()

startTime = (float)clock()/CLOCKS_PER_SEC;

/* Do work */

endTime = (float)clock()/CLOCKS_PER_SEC;

timeElapsed = endTime - startTime;

4
这应该成为被采纳的答案,而不是针对Windows的特定答案! - Simon
clock() 返回的是 CPU 时间而不是墙钟时间,如果在基准测试时有多个线程执行代码,这可能会让你感到惊讶。 - neevek
@neevek 但是如果你只有一个线程,使用CPU时间会得到正确的结果,因为如果你使用挂钟时间,你的系统负载将影响基准测试结果。如果你的系统正在执行后台任务,使用挂钟时间会得到比没有执行后台任务时更差的基准测试结果,但是使用CPU时间会得到相同的结果。 - Mecki
精度如何呢?通过执行基准代码N次并将测量的时间除以N,我们可以提高精度。我们如何确定N和精度? - chmike

51

基本上,你需要的是一个高分辨率的计时器。已经过去的时间当然只是时间差,加速比是通过将每个任务的时间相除来计算的。我已经包含了一个高分辨率计时器的代码,应该可以在至少windows和unix系统上工作。

#ifdef WIN32

#include <windows.h>
double get_time()
{
    LARGE_INTEGER t, f;
    QueryPerformanceCounter(&t);
    QueryPerformanceFrequency(&f);
    return (double)t.QuadPart/(double)f.QuadPart;
}

#else

#include <sys/time.h>
#include <sys/resource.h>

double get_time()
{
    struct timeval t;
    struct timezone tzp;
    gettimeofday(&t, &tzp);
    return t.tv_sec + t.tv_usec*1e-6;
}

#endif

7
墙上时间(由gettimeofday返回)可能并不那么有用 - clock_gettime(CLOCK_PROCESS_CPUTIME_ID, ...)通常是所需的。 - caf
6
一个程序如果只占用很少的CPU时间,但是大量时间用于阻塞I/O或等待异步I/O,用户仍可能感觉它运行缓慢。CPU时间和墙钟时间都很重要。 - bk1e
11
是的,这就是为什么我在我的评论中用了“可能”和“经常”这样含糊的措辞;顺便说一句,如果确实需要墙上时间,那么使用clock_gettime(CLOCK_MONOTONIC, ...)会更好,因为与gettimeofday不同,它不会受到计时间隔内系统时钟变化的影响。 - caf
1
顺便提一下,不应该每次都调用QueryPerformanceFrequency。 - Joe
如果在需要计时的过程之前执行了 a = get_time(),并在过程完成后执行了 b = get_time(),那么 (a - b) 代表什么意思?这个时间是以秒为单位计算的吗? - galois
显示剩余3条评论

6

轻松进行C代码基准测试

#include <time.h>

int main(void) {
  clock_t start_time = clock();

  // code or function to benchmark

  double elapsed_time = (double)(clock() - start_time) / CLOCKS_PER_SEC;
  printf("Done in %f seconds\n", elapsed_time);
}

多线程C代码的简单基准测试

如果您想对多线程程序进行基准测试,首先需要仔细查看clock

描述

clock()函数返回程序使用的处理器时间的近似值。

返回值

返回的值是CPU时间作为clock_t; 为了获得使用的秒数,需要除以CLOCKS_PER_SEC。如果不可用或其值无法表示所使用的处理器时间,则该函数返回值(clock_t)(-1)

因此,非常重要的是将经过的时间除以线程数量,以获取函数的执行时间:

#include <time.h>
#include <omp.h>

#define THREADS_NB omp_get_max_threads()

#pragma omp parallel for private(i) num_threads(THREADS_NB)
clock_t start_time = clock();

// code or function to benchmark

double elapsed_time = (double)(clock() - start_time) / CLOCKS_PER_SEC;
printf("Done in %f seconds\n", elapsed_time / THREADS_NB); // divide by THREADS_NB!

示例

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

#define N 20000
#define THREADS_NB omp_get_max_threads()

void init_arrays(double *a, double *b) {
  memset(a, 0, sizeof(a));
  memset(b, 0, sizeof(b));
  for (int i = 0; i < N; i++) {
    a[i] += 1.0;
    b[i] += 1.0;
  }
}

double func2(double i, double j) {
  double res = 0.0;

  while (i / j > 0.0) {
    res += i / j;
    i -= 0.1;
    j -= 0.000003;
  }
  return res;
}

double single_thread(double *a, double *b) {
  double res = 0;
  int i, j;
  for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
      if (i == j) continue;
      res += func2(a[i], b[j]);
    }
  }
  return res;
}

double multi_threads(double *a, double *b) {
  double res = 0;
  int i, j;
  #pragma omp parallel for private(j) num_threads(THREADS_NB) reduction(+:res)
  for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
      if (i == j) continue;
      res += func2(a[i], b[j]);
    }
  }
  return res;
}

int main(void) {
  double *a, *b;
  a = (double *)calloc(N, sizeof(double));
  b = (double *)calloc(N, sizeof(double));
  init_arrays(a, b);

  clock_t start_time = clock();
  double res = single_thread(a, b);
  double elapsed_time = (double)(clock() - start_time) / CLOCKS_PER_SEC;
  printf("Default:  Done with %f in %f sd\n", res, elapsed_time);

  start_time = clock();
  res = multi_threads(a, b);
  elapsed_time = (double)(clock() - start_time) / CLOCKS_PER_SEC;
  printf("With OMP: Done with %f in %f sd\n", res, elapsed_time / THREADS_NB);
}

使用以下命令进行编译:

gcc -O3 multithread_benchmark.c -fopenmp && time ./a.out

输出:

Default:  Done with 2199909813.614555 in 4.909633 sd
With OMP: Done with 2199909799.377532 in 1.708831 sd

real    0m6.703s (from time function)

2
你难道没有假设所有线程在任何时候都可以充分利用所有核心吗?因此,如果存在任何同步开销,您会低估实际时间的数量。如果您想要实时性,请使用 clock_gettime 要求实时,并在空闲系统上进行测试。然后,您可以比较在该实时量期间使用的CPU时间总秒数。或者最小化启动开销,使基准测试重复足够多次以支配整个运行时,并 perf stat 您的整个程序,它将为您完成所有这些工作,包括显示 task-clock 和3.800个 CPU 已利用或其他内容。 - Peter Cordes
我不得不做出这个假设,因为没有办法知道当前活动线程的数量,否则会减慢程序的运行速度 ;) 这只是为了得到一个公平的估计,并且它可以工作,但绝不是精确的。 - Antonin GAVREL
1
只有在坚持通过某些略微不太便携的高分辨率时间源直接测量实时而不是外推时,你才“必须”这样做。我不建议这样做;就像我说的那样,使用适当的实时时钟而不是任何外推。你可能会欺骗自己并隐藏任何串行或较少并行阶段的问题,这些问题不能完美地分成均匀大小的块,并具有相同数量的工作量。(不同的OpenMP调度选项可用于处理此类问题,例如动态与静态。) - Peter Cordes
1
memset(a, 0, sizeof(a)) 是不正确的。应该写成 memset(a, 0, sizeof(*a) * N),并且N 应该作为参数传递,尽管如果 N 是一个可变数量,这会使编译器更难以并行化代码。 - chqrlie
1
你对OMP计时的方法是值得怀疑的:要么你只关心单个执行线程的性能,那么可以通过不并行化代码生成来实现;要么你想评估OMP代码生成器的效率,那么应该同时报告墙上时钟计时和请求与使用的实际线程数。仅仅除以数量会削减关键信息。你应该将计时乘以实际线程数,并与单线程计时进行比较,看看OMP是否高效甚至有用。 - chqrlie
有没有一种简单的方法来获取请求和使用的线程数? - Antonin GAVREL

1
在POSIX中,尝试使用getrusage。相关参数是RUSAGE_SELF,相关字段是ru_utime.tv_sec和ru_utime.tv_usec。

1
请注意,许多使用procfs的Unix操作系统实际上没有实现此功能。(Linux,Solaris) - charliehorse55

0

可能已经有现有的工具可以帮助解决这个问题,但我怀疑大多数人会使用一些采样或可能是注入的方法。但是要计时特定代码段,您可能需要添加调用类似于您在示例中显示的计时器的调用。如果您正在使用Windows,则高性能计时器可用。我回答了一个类似的问题并展示了可以实现此功能的示例代码。Linux也有类似的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接