基准测试一个纯C++函数

Benchmarking a pure C++ function

本文关键字:C++ 函数 一个 基准测试      更新时间:2023-10-16

如何防止GCC/Clang内联和优化纯函数的多次调用?

我正在尝试对这种形式的代码进行基准测试

int __attribute__ ((noinline)) my_loop(int const* array, int len) {
   // Use array to compute result.
 }

我的基准代码看起来像这样:

int main() {
  const int number = 2048;
   // My own aligned_malloc implementation.
  int* input = (int*)aligned_malloc(sizeof(int) * number, 32);
  // Fill the array with some random numbers.
  make_random(input, number);
  const int num_runs = 10000000;
  for (int i = 0; i < num_runs; i++) {
     const int result = my_loop(input, number); // Call pure function.
  }
  // Since the program exits I don't free input.
}

正如预期的那样,Clang似乎能够在O2(甚至可能在O1(将其转变为无操作。

我试着对我的实现进行基准测试的几件事是:

  • 将中间结果累加为整数,并在最后打印结果:

    const int num_runs = 10000000;
    uint64_t total = 0;
    for (int i = 0; i < num_runs; i++) {
      total += my_loop(input, number); // Call pure function.
    }
    printf("Total is %llun", total);
    

    遗憾的是,这似乎不起作用。Clang至少足够聪明,意识到这是一个纯粹的函数,并将基准转换为这样的东西:

    int result = my_loop();
    uint64_t total = num_runs * result;
    printf("Total is %llun", total);
    
  • 在每次循环迭代结束时使用发布语义设置一个原子变量:

    const int num_runs = 10000000;
    std::atomic<uint64_t> result_atomic(0);
    for (int i = 0; i < num_runs; i++) {
      int result = my_loop(input, number); // Call pure function.
      // Tried std::memory_order_release too.
      result_atomic.store(result, std::memory_order_seq_cst);
    }
    printf("Result is %llun", result_atomic.load());
    

    我的希望是,由于原子论引入了happens-before关系,Clang将被迫执行我的代码。但遗憾的是,它仍然进行了上述优化,并一次性将原子的值设置为num_runs * result,而不是运行函数的num_runs迭代。

  • 在每个循环的末尾设置一个volatile int,同时求和总数。

    const int num_runs = 10000000;
    uint64_t total = 0;
    volatile int trigger = 0;
    for (int i = 0; i < num_runs; i++) {
      total += my_loop(input, number); // Call pure function.
      trigger = 1;
    }
    // If I take this printf out, Clang optimizes the code away again.
    printf("Total is %llun", total);
    

    这似乎起到了作用,我的基准测试似乎有效。这并不理想,原因有很多。

  • 根据我对C++11内存模型volatile set operations的理解,它没有建立happens before关系,所以我不能确定某些编译器不会决定进行相同的num_runs * result_of_1_run优化。

  • 此外,这种方法似乎是不可取的,因为现在我在每次运行循环时都要设置一个volatile int,这是一个开销(无论多么小(。

有没有一种规范的方法可以阻止Clang/GCC优化这个结果。也许是用实用主义者什么的?如果这种理想的方法在编译器中有效,则可获得额外的积分。

您可以直接在程序集中插入指令。我有时会使用宏来拆分程序集,例如将负载与计算和分支分离。

#define GCC_SPLIT_BLOCK(str)  __asm__( "//nt// " str "nt//n" );

然后在源中插入

GCC_SPLIT_BLOCK("请保管"(

在您的函数之前和之后