微基准测试 C 代码和缓存效果

问题描述 投票:0回答:1
我在 macOS Sonoma 上使用 M1 Pro,使用

clang

 15 (
clang-1500.0.40.1
),编译时没有任何(显式)优化。 
(编辑:使用 -O3,我可以观察到各个版本之间没有有意义的差异。不过,最后一个版本似乎比以前运行的同一版本要快一些。)

我编写了一个小函数来洗牌数组,我想测试它的性能。为此,我编写了以下函数:

void test_performance() { // initialize only once to measure performance int ns[] = {1, 2, 3, 4, 5}; int ops = 1E6; int start = clock(); for (int i = 0; i < ops; i++) { shuffle(ns, 5); } int end = clock(); double s_total = ((double) (end - start)) / CLOCKS_PER_SEC; double ns_op = s_total / ops * 1E9; printf("test_performance: %fns/op (%fs total) \n", ns_op, s_total); }
在我的机器上,这会产生 ~30ns/op。

然后我意识到循环操作包含在测量中。我虽然它可能几乎可以忽略不计,但我可以摆脱那个小噪音,所以我将其更改为在循环内调用

clock()

 并将在 
shuffle 上花费的 实际
时间添加到累加器,结果在下面的代码中。

void test_performance() { // initialize only once to measure performance int ns[] = {1, 2, 3, 4, 5}; int ops = 1E6; int start, end, ticks = 0; for (int i = 0; i < ops; i++) { start = clock(); shuffle(ns, 5); end = clock(); ticks += (end - start); } double s_total = ((double) ticks) / CLOCKS_PER_SEC; double ns_op = s_total / ops * 1E9; printf("test_performance: %fns/op (%fs total) \n", ns_op, s_total); }
程序整体运行速度明显变慢,这并不奇怪,因为现在我对 

clock()

 的调用增加了几百万次。然而,令我惊讶的是,最终报告的每次操作时间增加了一个数量级,大约为 330ns/op。

第一个问题:什么可能导致如此巨大的差异?

我最好的猜测是,他与现代硬件、缓存以及

clock()

 可能涉及系统调用这一事实有关,在一种情况下 
shuffle
 的代码始终位于 CPU 缓存中,并且在其他,但我不太确定情况是否如此,或者是否有更明显的东西我遗漏了。

第二个问题:如果是这样的话,使用 C 语言进行微基准测试和评估缓存影响时有哪些好的做法?我主要感兴趣的是在不需要专门工具的情况下制作相对强大的基准测试,但我也很高兴听到 C 社区使用的微基准测试工具。

编辑

有趣的是,我尝试使用

clock_gettime

,它还允许明确要求单调递增的时间,最终得到以下结果:

void test_performance() { struct timespec start_time, end_time; // initialize only once to measure performance int ns[] = {1, 2, 3, 4, 5}; int ops = 1E6; double s_total = 0; for (int i = 0; i < ops; i++) { if (clock_gettime(CLOCK_MONOTONIC, &start_time) != 0) { perror("clock_gettime"); return; } shuffle(ns, 5); if (clock_gettime(CLOCK_MONOTONIC, &end_time) != 0) { perror("clock_gettime"); return; } s_total += (end_time.tv_sec - start_time.tv_sec) + (end_time.tv_nsec - start_time.tv_nsec) / 1.0e9; } double ns_op = s_total / ops * 1E9; printf("test_performance: %fns/op (%fs total) \n", ns_op, s_total); }
有趣的是,这段代码报告的时间约为 25 纳秒。当我尝试对整个循环进行时间测量而不是测量单个调用时,报告的时间回到了约 30 纳秒,就像我使用 

clock() 进行的第一个实验一样。

我很想了解为什么 
clock_gettime

似乎不会造成与

clock()

 相同的惩罚。

c performance performance-testing benchmarking microbenchmark
1个回答
1
投票

很难说,因为禁用优化的基准测试几乎没有意义,您必须查看生成的机器代码才能更好地理解事情。

每次迭代执行系统调用并中断循环两次意味着执行大量您并不真正想要计时的无用指令,这很容易破坏处理器管道,从而大大降低性能。编译器可能能够也可能无法以优化的方式对指令进行排序以避免这种情况。在你的情况下,它可能没有(这是有道理的,因为你告诉它你不关心优化)。

它还可能导致操作系统进行更多的重新调度,因为系统调用入口/出口通常是常见的调度程序入口点,您的任务可以在其中为其他任务设置一边运行,而不需要发生内核抢占(就像您执行紧密的用户空间循环一样)没有系统调用)。

我很想了解为什么

clock_gettime
似乎不会造成与

clock()

 相同的惩罚。
在这里查看答案:

macOS 上的 gettimeofday() 使用系统调用吗?

clock_gettime(CLOCK_MONOTONIC)

可能只是

gettimeofday

 的包装,它通过用户空间采用快速路径,并且不需要上下文切换。这是对简单且常用的系统调用的常见优化。
我不太了解 macOS 如何实现其“commpage”,用于在没有上下文切换的情况下执行一些常见的系统调用,但我认为它与 Linux 实现“
vDSO

”的方式非常相似。

© www.soinside.com 2019 - 2024. All rights reserved.