对上述问题的简单回答,“如何将 TSC 频率转换为微秒或毫秒?”是:你没有。 TSC(时间戳计数器)时钟频率的实际值取决于硬件,并且在某些硬件上可能会在运行时发生变化。要实时测量,您可以使用clock_gettime(CLOCK_REALTIME)
or clock_gettime(CLOCK_MONOTONIC)
在Linux中。
正如 Peter Cordes 在评论(2018 年 8 月)中提到的,在大多数当前的 x86-64 架构上,时间戳计数器(通过 RDTSC 指令访问)__rdtsc()
函数声明于<x86intrin.h>
)计算参考时钟周期,而不是 CPU 时钟周期。他的回答C++中的类似问题 https://stackoverflow.com/a/51907627/1475978对于 x86-64 上的 Linux 中的 C 也有效,因为编译器在编译 C 或 C++ 时提供底层内置,而答案的其余部分涉及硬件详细信息。我也建议您阅读那一篇。
这个答案的其余部分假设根本问题是微基准测试代码,以找出某个函数的两个实现如何相互比较。
在 x86(Intel 32 位)和 x86-64(AMD64、Intel 和 AMD 64 位)架构上,您可以使用__rdtsc()
from <x86intrin.h>
找出经过的 TSC 时钟周期数。这可用于测量和比较某些功能的不同实现所使用的周期数,通常是很多次。
请注意,TSC 时钟与 CPU 时钟的关系存在硬件差异。上述最近的答案对此进行了一些详细介绍。出于 Linux 中的实际目的,在 Linux 中使用就足够了cpufreq-set
禁用频率缩放(以确保 CPU 和 TSC 频率之间的关系在微基准测试期间不会改变),并且可以选择taskset
将微基准测试限制为特定的 CPU 核心。这确保了在该微基准测试中收集的结果产生可以相互比较的结果。
(正如 Peter Cordes 评论的那样,我们还想添加_mm_lfence()
from <emmintrin.h>
(包括由<immintrin.h>
)。这可确保 CPU 不会在内部对 RDTSC 操作与要进行基准测试的函数进行重新排序。您可以使用-DNO_LFENCE
如果需要,可以在编译时忽略这些。)
假设你有函数void foo(void);
and void bar(void);
您想要比较的:
#include <stdlib.h>
#include <x86intrin.h>
#include <stdio.h>
#ifdef NO_LFENCE
#define lfence()
#else
#include <emmintrin.h>
#define lfence() _mm_lfence()
#endif
static int cmp_ull(const void *aptr, const void *bptr)
{
const unsigned long long a = *(const unsigned long long *)aptr;
const unsigned long long b = *(const unsigned long long *)bptr;
return (a < b) ? -1 :
(a > b) ? +1 : 0;
}
unsigned long long *measure_cycles(size_t count, void (*func)())
{
unsigned long long *elapsed, started, finished;
size_t i;
elapsed = malloc((count + 2) * sizeof elapsed[0]);
if (!elapsed)
return NULL;
/* Call func() count times, measuring the TSC cycles for each call. */
for (i = 0; i < count; i++) {
/* First, let's ensure our CPU executes everything thus far. */
lfence();
/* Start timing. */
started = __rdtsc();
/* Ensure timing starts before we call the function. */
lfence();
/* Call the function. */
func();
/* Ensure everything has been executed thus far. */
lfence();
/* Stop timing. */
finished = __rdtsc();
/* Ensure we have the counter value before proceeding. */
lfence();
elapsed[i] = finished - started;
}
/* The very first call is likely the cold-cache case,
so in case that measurement might contain useful
information, we put it at the end of the array.
We also terminate the array with a zero. */
elapsed[count] = elapsed[0];
elapsed[count + 1] = 0;
/* Sort the cycle counts. */
qsort(elapsed, count, sizeof elapsed[0], cmp_ull);
/* This function returns all cycle counts, in sorted order,
although the median, elapsed[count/2], is the one
I personally use. */
return elapsed;
}
void benchmark(const size_t count)
{
unsigned long long *foo_cycles, *bar_cycles;
if (count < 1)
return;
printf("Measuring run time in Time Stamp Counter cycles:\n");
fflush(stdout);
foo_cycles = measure_cycles(count, foo);
bar_cycles = measure_cycles(count, bar);
printf("foo(): %llu cycles (median of %zu calls)\n", foo_cycles[count/2], count);
printf("bar(): %llu cycles (median of %zu calls)\n", bar_cycles[count/2], count);
free(bar_cycles);
free(foo_cycles);
}
请注意,上述结果非常特定于所使用的编译器和编译器选项,当然还取决于运行它的硬件。周期中位数可以解释为“所采用的 TSC 周期的典型数量”,因为测量结果并不完全可靠(可能会受到进程外部事件的影响;例如,通过上下文切换或迁移到另一个核心)一些CPU)。出于同样的原因,我不相信最小值、最大值或平均值。
然而,这两个实现'(foo()
and bar()
) 以上循环计数can在微基准测试中进行比较,以了解它们的性能如何相互比较。请记住,微基准测试结果可能不会扩展到实际工作任务,因为任务的资源使用交互非常复杂。一个函数可能在所有微基准测试中都表现出色,但在现实世界中却比其他函数差,因为它只有在有大量 CPU 缓存可供使用时才有效。
一般来说,在 Linux 中,您可以使用CLOCK_REALTIME
使用时钟来测量实时时间(挂钟时间),其方式与上述相同。CLOCK_MONOTONIC
甚至更好,因为它不受管理员可能对实时时钟进行的直接更改的影响(例如,如果他们注意到系统时钟提前或落后);仅应用由于 NTP 等引起的漂移调整。夏令时或其变化不会影响使用任一时钟的测量。同样,多次测量的中位数是我寻求的结果,因为测量代码本身之外的事件可能会影响结果。
例如:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#ifdef NO_LFENCE
#define lfence()
#else
#include <emmintrin.h>
#define lfence() _mm_lfence()
#endif
static int cmp_double(const void *aptr, const void *bptr)
{
const double a = *(const double *)aptr;
const double b = *(const double *)bptr;
return (a < b) ? -1 :
(a > b) ? +1 : 0;
}
double median_seconds(const size_t count, void (*func)())
{
struct timespec started, stopped;
double *seconds, median;
size_t i;
seconds = malloc(count * sizeof seconds[0]);
if (!seconds)
return -1.0;
for (i = 0; i < count; i++) {
lfence();
clock_gettime(CLOCK_MONOTONIC, &started);
lfence();
func();
lfence();
clock_gettime(CLOCK_MONOTONIC, &stopped);
lfence();
seconds[i] = (double)(stopped.tv_sec - started.tv_sec)
+ (double)(stopped.tv_nsec - started.tv_nsec) / 1000000000.0;
}
qsort(seconds, count, sizeof seconds[0], cmp_double);
median = seconds[count / 2];
free(seconds);
return median;
}
static double realtime_precision(void)
{
struct timespec t;
if (clock_getres(CLOCK_REALTIME, &t) == 0)
return (double)t.tv_sec
+ (double)t.tv_nsec / 1000000000.0;
return 0.0;
}
void benchmark(const size_t count)
{
double median_foo, median_bar;
if (count < 1)
return;
printf("Median wall clock times over %zu calls:\n", count);
fflush(stdout);
median_foo = median_seconds(count, foo);
median_bar = median_seconds(count, bar);
printf("foo(): %.3f ns\n", median_foo * 1000000000.0);
printf("bar(): %.3f ns\n", median_bar * 1000000000.0);
printf("(Measurement unit is approximately %.3f ns)\n", 1000000000.0 * realtime_precision());
fflush(stdout);
}
一般来说,我个人更喜欢在单独的单元中编译基准测试函数(到单独的目标文件),并对不执行任何操作的函数进行基准测试以估计函数调用开销(尽管它往往会高估开销;即产生太大的开销估计,因为一些函数调用开销是延迟而不是实际花费的时间,并且在实际功能的这些延迟期间可以进行一些操作)。
重要的是要记住,上述测量值只能用作指示,因为在现实世界的应用程序中,诸如缓存局部性之类的东西(特别是在当前的机器上,具有多级缓存和大量内存)会极大地影响不同的实现。
例如,您可以比较快速排序和基数排序的速度。根据键的大小,基数排序需要相当大的额外数组(并使用大量缓存)。如果使用排序例程的实际应用程序不会同时使用大量其他内存(因此排序的数据基本上是缓存的数据),那么如果有足够的数据(并且实现是合理的),则基数排序会更快)。但是,如果应用程序是多线程的,并且其他线程混洗(复制或传输)大量内存,则使用大量缓存的基数排序将逐出也缓存的其他数据;即使基数排序函数本身没有表现出任何严重的减速,它可能会减慢其他线程的速度,从而减慢整个程序的速度,因为其他线程必须等待它们的数据被重新缓存。
这意味着您应该信任的唯一“基准”是实际硬件上使用的挂钟测量值,使用实际工作数据运行实际工作任务。其他一切都受到许多条件的影响,并且或多或少令人怀疑:有迹象表明,是的,但不是很可靠。