[英]measuring code execution times in C using RDTSC instruction
我编写了一个简单的程序来使用RDTSC指令测量代码执行时间。 但是我不知道我的结果是否正确以及我的代码是否有任何错误...我不知道如何进行验证。
#include <stdio.h>
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#define N (1024*4)
unsigned cycles_low, cycles_high, cycles_low1, cycles_high1;
static __inline__ unsigned long long rdtsc(void)
{
__asm__ __volatile__ ("RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "rbx", "rcx", "rdx");
}
static __inline__ unsigned long long rdtsc1(void)
{
__asm__ __volatile__ ("RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "rbx", "rcx", "rdx");
}
int main(int argc, char* argv[])
{
uint64_t start, end;
rdtsc();
malloc(N);
rdtsc1();
start = ( ((uint64_t)cycles_high << 32) | cycles_low );
end = ( ((uint64_t)cycles_high1 << 32) | cycles_low1 );
printf("cycles spent in allocating %d bytes of memory: %llu\n",N, end - start);
return 0;
}
使用RDTSC计时时,应牢记一些(非显而易见的)问题:
它所计数的时钟频率可能无法预测。 在较旧的硬件上,频率实际上可能会在两个RDTSC指令之间变化,甚至在固定了该频率的较新的硬件上,也很难分辨其运行频率。
由于RDTSC没有输入,因此CPU本身可能会重新排序RDTSC指令,使其位于您要测量的代码之前。 请注意,这是与编译器对代码重新排序不同的问题,而您使用__volatile__可以避免此问题。 为了有效避免这种情况,您必须执行序列化指令 ,该指令将阻止CPU在其之前移动指令。 您可以使用CPUID或RDTSCP(这只是RDTSC的序列化形式)
我的建议:只需使用您的操作系统具有的任何高频定时器API。 在Windows上,这是QueryPerformanceCounter;在Unix上,您有gettimeofday或clock_gettime。
除此之外,您的RDTSC代码还有一些结构性问题。 返回类型为“ unsigned long long”,但实际上未返回任何内容。 如果解决此问题,则可以避免将结果存储在全局变量中,并且可以避免编写多个版本。
可能影响您获得结果的问题是:
在大多数现代80x86 CPU上,TSC测量的是固定频率的时钟而不是周期,因此,根据电源管理,同一内核中其他逻辑CPU的负载(超线程),同一段代码可能具有截然不同的“周期”,其他核心的负载(涡轮增压),CPU温度(热调节)等。
没有什么可以阻止操作系统的调度程序在第一个rdtsc();
之后立即rdtsc();
您的线程rdtsc();
导致产生的“分配周期”包括CPU执行任何数量完全不同的进程所花费的时间。
在某些计算机上,不同CPU上的TSC无法同步; 并没有什么可以阻止操作系统在第一个rdtsc();
之后立即rdtsc();
线程rdtsc();
然后在完全不同的CPU(具有完全不同的TSC)上运行线程。 在这种情况下, end - start
可能是end - start
-为负(例如时间倒退)。
没有什么能阻止IRQ(来自硬件)在第一个rdtsc();
之后立即中断您的代码rdtsc();
导致产生的“花费的分配周期”包括操作系统花在处理任意数量的IRQ上的时间。
在第一个rdtsc();
之后,不可能防止SMI(“系统管理中断”)导致CPU进入SMM(“系统管理模式”)并执行隐藏的固件代码rdtsc();
导致产生的“花费的分配周期”包括CPU花费在执行固件代码上的时间。
一些(旧)CPU存在一个错误, rdtsc
当低32位溢出时rdtsc
会给出rdtsc
结果(例如,当TSC从0x00000000FFFFFFFF变为0x0000000100000000时,您可以在确切的错误时间使用rdtsc
并获得0x0000000000000000)。
没有什么可以阻止“乱序”的现代CPU重新排列大多数指令(包括rdtsc
指令)的执行顺序。
您的测量包括测量的开销(例如,如果rdtsc
花费5个周期,而malloc()
花费20个周期,那么您报告25个周期而不是20个周期)。
有或没有虚拟机; 可能rdtsc
指令进行虚拟化(例如,除了常识之外,其他任何措施都无法阻止内核使rdtsc
报告存在多少可用磁盘空间或它喜欢的其他任何东西)。 理想情况下,应该对rdtsc
进行虚拟化,以防止出现上述大多数问题和/或防止对副信道进行计时(但几乎永远不会这样)。
在极旧的CPU(80486及更早版本)上,TSC和rdtsc
指令不存在。
注意:我不是GCC内联汇编的专家; 但我强烈怀疑您的宏存在错误,并且编译器可以选择生成如下内容:
rdtsc
mov %edx, %eax ;Oops, trashed the low 32 bits
mov %eax, %ebx
应该可以告诉GCC,值是在EDX:EAX中返回的,并且完全摆脱了这两个mov
指令。
注意:在撰写本文时,我想出了一种更简单/更简洁的方法来校准TSC
转换因子。 所以,继续阅读...
如果愿意,可以在/proc/cpuinfo
的linux下[某些其他操作系统具有类似功能-例如BSD实现linux / proc的一部分],您将看到类似以下的字段:
bogomips : 5306.71
flags : blah blah2 constant_tsc
processor : blah
如果您读取此文件,则bogomips
是系统引导过程中计算的以Mhz [sort of]为单位的CPU总频率。 如果您的计算机具有速度步进,则最好使用cpu Mhz
。
要使用bogomips
,请计算processor
行数,然后将其除以bogomips
。 注意去除“。”。 并将其视为Khz并使用整数数学。
如果您具有constant_tsc
,则TSC
将始终以此[最大]频率运行,并且永远不会变化,无论某个特定的内核是否由于速度步进而变慢。
如果阅读/proc/cpuinfo
使您感到尴尬,则可以使用另一种方法来校准/确定TSC
频率。
请执行下列操作:
tsc1 = rdtsc
clk1 = clock_gettime
// delay for a while
for (i = 1; i < 1000000; ++i)
asm volatile ("" ::: "memory");
clk2 = clock_gettime
tsc2 = rdtsc
使用这些值,您可以计算TSC
频率。 做以上几千遍。 采取最小增量-这可以防止OS时间将您切分的那些测量结果。
将最大值用作不会引起时间片的循环计数。 实际上,您可以用tv_sec = 0, tv_nsec = 500000
(500 us)的nanosleep
替换循环。 nanosleep
比当量好得多usleep
。 实际上,如果您nanosleep
,您可以nanosleep
2-3秒。
clk2 - clk2
值[转换为]秒分数,可为tsc2 - tsc1
校准,以及与TSC
滴答和秒之间的转换。
对于32位平台,存在“ = A”。 这将从eax和edx创建64位结果。 可悲的是,在64位平台上,它仅表示rax寄存器,这无济于事。
相反,更好的是,您可以使用“ __builtin_ia32_rdtsc()”内部函数来直接返回64位无符号整数。 对于rdtscp(也返回当前内核)类似。 请参阅gcc手册。 与使用内联汇编手动完成相比,它们确实发出了更好的代码,并且可以在32位和64位之间移植。
如果在/ proc / cpuinfo标志中设置了“ constant_tsc”,则无论任何CPU频率缩放比例,TSC都将以恒定速率运行。 如果设置了“ nonstop_tsc”,则TSC继续在C(休眠)状态下运行。 如果两者都设置,则计数器“也应该”在内核之间同步(至少在最新的CPU,Core i7或更高版本上)。 我对最后一个不太确定,也许有人可以纠正我?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.