使用背靠背rdtsc进行负时钟周期测量?

我正在编写一个C代码,用于测量获取信号量所需的时钟周期数。 我正在使用rdtsc,在对信号量进行测量之前,我连续两次调用rdtsc来测量开销。 我在for循环中重复了这么多次,然后我使用平均值作为rdtsc开销。

这是正确的,首先要使用平均值吗?

尽管如此,这里的一个大问题是,有时我会得到开销的负值(不一定是平均值,但至少是for循环中的部分值)。

这也会影响sem_wait()操作所需的cpu周期数的连续计算,有时也会产生负数。 如果我写的不清楚,这里有一部分我正在编写的代码。

为什么我会得到这样的负值?


(编者注:请参阅获取CPU周期计数?以获得完整的64位时间戳的正确和可移植方式。 "=A" asm约束仅在编译为x86-64时获得低或高32位,具体取决于寄存器分配是否恰好为uint64_t输出选择RAX或RDX。它不会选择edx:eax 。)

(编辑的第二个注释:哎呀,这就是为什么我们得到负面结果的答案。仍然值得留下一个注释,作为警告不要复制这个rdtsc实现。)


 #include  #include  #include  #include  #include  static inline uint64_t get_cycles() { uint64_t t; // editor's note: "=A" is unsafe for this in x86-64 __asm volatile ("rdtsc" : "=A"(t)); return t; } int num_measures = 10; int main () { int i, value, res1, res2; uint64_t c1, c2; int tsccost, tot, a; tot=0; for(i=0; i<num_measures; i++) { c1 = get_cycles(); c2 = get_cycles(); tsccost=(int)(c2-c1); if(tsccost<0) { printf("#### ERROR!!! "); printf("rdtsc took %d clock cycles\n", tsccost); return 1; } tot = tot+tsccost; } tsccost=tot/num_measures; printf("rdtsc takes on average: %d clock cycles\n", tsccost); return EXIT_SUCCESS; } 

当英特尔首次发明TSC时,它测量了CPU周期。 由于各种电源管理function,“每秒周期数”不是恒定的; 所以TSC最初很适合测量代码的性能(并且不利于测量时间)。

无论好坏; 那时CPU并没有真正有太多的电源管理,无论如何CPU经常以固定的“每秒周期”运行。 一些程序员错误地想法并误用了TSC来测量时间而不是周期。 后来(当电源管理function的使用变得越来越普遍时)这些人滥用TSC来测量他们滥用造成的所有问题的时间。 CPU制造商(从AMD开始)改变了TSC,因此它测量时间而不是周期(使其在测量代码性能时被破坏,但对于测量时间的测量是正确的)。 这引起了混淆(软件很难确定TSC实际测量的是什么),所以稍后AMD就在CPUID上添加了“TSC Invariant”标志,因此如果设置了这个标志,程序员就知道TSC坏了(用于测量)循环)或固定(用于测量时间)。

英特尔跟随AMD并改变了他们的TSC行为以测量时间,并采用了AMD的“TSC Invariant”标志。

这给出了4种不同的情况:

  • TSC测量时间和性能(每秒周期数不变)

  • TSC衡量的是绩效而非时间

  • TSC测量时间而不是性能,但不使用“TSC Invariant”标志来表示

  • TSC测量时间而不是性能,并使用“TSC Invariant”标志来表示(大多数现代CPU)

对于TSC测量时间的情况,要正确测量性能/周期,您必须使用性能监控计数器。 遗憾的是,性能监视计数器对于不同的CPU(特定于模型)是不同的,并且需要访问MSR(特权代码)。 这使得应用程序测量“循环”非常不切实际。

另请注意,如果TSC确实测量时间,则无法使用其他时间源确定缩放因子,无法知道它返回的时间刻度(“假装周期”中的纳秒数)。

第二个问题是,对于多CPU系统,大多数操作系统都很糟糕。 操作系统处理TSC的正确方法是防止应用程序直接使用它(通过在CR4中设置TSD标志;以便RDTSC指令导致exception)。 这可以防止各种安全漏洞(定时侧通道)。 它还允许操作系统模拟TSC并确保它返回正确的结果。 例如,当应用程序使用RDTSC指令并导致exception时,OS的exception处理程序可以确定要返回的正确“全局时间戳”。

当然,不同的CPU都有自己的TSC。 这意味着如果应用程序直接使用TSC,则它们会在不同的CPU上获得不同的值。 帮助人们解决操作系统无法解决问题的方法(通过仿效RDTSC); AMD增加了RDTSCP指令,返回TSC和“处理器ID”(英特尔也最终采用了RDTSCP指令)。 在损坏的操作系统上运行的应用程序可以使用“处理器ID”来检测它们何时在上一次运行在不同的CPU上; 并且以这种方式(使用RDTSCP指令),他们可以知道“elapsed = TSC – previous_TSC”何时给出有效结果。 然而; 该指令返回的“处理器ID”只是MSR中的一个值,OS必须将每个CPU上的该值设置为不同的值 – 否则RDTSCP将在所有CPU上说“处理器ID”为零。

基本上; 如果CPU支持RDTSCP指令,并且操作系统已正确设置“处理器ID”(使用MSR); 那么RDTSCP指令可以帮助应用程序知道他们何时遇到了糟糕的“经过时间”结果(但它无法提供修复或避免不良结果)。

所以; 长话短说,如果你想要一个准确的性能测量,你大多是搞砸了。 您真正希望的最好的是准确的时间测量; 但仅限于某些情况下(例如,在单CPU机器上运行或“固定”到特定CPU;或者在操作系统上使用RDTSCP时,只要您检测并丢弃无效值,就可以正确设置它)。

当然,即使这样,你也会因为像IRQ这样的东西而得到狡猾的测量。 为此原因; 最好在循环中多次运行代码并丢弃任何比其他结果高得多的结果。

最后,如果你真的想要正确地做,你应该衡量测量的开销。 要做到这一点,你需要测量什么都不做的时间(仅仅是RDTSC / RDTSCP指令,同时丢弃狡猾的测量值); 然后从“测量某事”结果中减去测量的开销。 这可以让您更好地估计实际需要的时间。

注意:如果您可以从Pentium首次发布时(20世纪90年代中期 – 不确定它是否已经在线提供 – 我自20世纪80年代以来已经存档)您可以发掘出英特尔系统编程指南的副本,您会发现英特尔记录了时间戳计数器“可用于监视和识别处理器事件发生的相对时间”。 他们保证(不包括64位环绕)它会单调增加(但不是它会以固定的速率增加)并且它需要至少10年才能完成。 该手册的最新版本更详细地记录了时间戳计数器,表明对于较旧的CPU(P6,Pentium M,较旧的Pentium 4),时间戳计数器“随每个内部处理器时钟周期递增”和“Intel(r) SpeedStep(r)技术转换可能会影响处理器时钟“; 而较新的CPU(较新的Pentium 4,Core Solo,Core Duo,Core 2,Atom)TSC以恒定速率递增(这就是“架构行为向前发展”)。 从本质上讲,它从一开始就是一个(可变的)“内部循环计数器”用于时间戳(而不是用于跟踪“挂钟”时间的时间计数器),这种行为在2000年(基于Pentium 4发布日期)。

  1. 不要使用平均值

    使用最小的一个或平均较小的值(由于CACHE而得到平均值),因为较大的值已被OS多任务中断。

    您还可以记住所有值,然后找到操作系统进程粒度边界并过滤掉此边界后的所有值(通常> 1ms ,很容易检测到)

    在此处输入图像描述

  2. 无需测量RDTSC开销

    你只需要在一段时间内进行测量,并且在两个时间内都存在相同的偏移,并且在减去之后它就消失了。

  3. 用于RDTS可变时钟源(如在笔记本电脑上)

    您应该通过一些稳定的密集计算循环将CPU的速度更改为最大值,通常只需几秒即可。 您应该连续测量CPU频率,并且只有在足够稳定时才开始测量您的物体。

如果您在一个处理器上启动代码然后切换到另一个处理器,则由于处理器hibernate等原因,时间戳差异可能为负。

在开始测量之前,请尝试设置处理器关联。

我无法看到你是在Windows或Linux下运行的问题,所以我会回答这两个问题。

视窗:

 DWORD affinityMask = 0x00000001L; SetProcessAffinityMask(GetCurrentProcessId(), affinityMask); 

Linux的:

 cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); sched_setaffinity (getpid(), sizeof(cpuset), &cpuset) 

其他答案很棒(去读它们),但假设正确读取rdtsc 。 这个答案正在解决内联asm错误,导致完全虚假的结果,包括否定。

另一种可能性是你将其编译为32位代码,但是有更多的重复,并且在没有不变的TSC(跨所有内核的同步TSC)的系统上偶尔出现CPU迁移的负间隔。 可以是多插槽系统,也可以是旧的多核系统。 CPU TSC提取操作,尤其是在多核多处理器环境中 。


如果您正在为x86-64进行编译,那么您的否定结果将由asm错误"=A"输出约束完全解释。 请参阅获取CPU周期数? 正确使用rdtsc的方法,可以移植到所有编译器和32位与64位模式。 或者使用"=a""=d"输出并简单地忽略高半输出,对于不会溢出32位的短间隔。)

(我很惊讶你没有提到它们也是巨大而且变化很大,并且即使没有单独的测量结果是负面的,也会溢出以给出负平均值。我看到平均值如-63421899 ,或69374170 ,或者115365476

使用gcc -O3 -m32进行编译使其按预期工作,打印平均值为24到26(如果在循环中运行,那么CPU保持最高速度,否则就像返回到24之间的24个核心时钟周期的125个参考周期回到Skylake的rdtsc )。 https://agner.org/optimize/用于指令表。


Asm详细说明"=A"约束出了什么问题

rdtsc (insn ref手动输入) 总是edx:eax产生两个32位hi:lo的64位结果,即使在64位模式下我们真的更喜欢它在一个64位寄存器中。

您期望"=A"输出约束为uint64_t t选择edx:eax 。 但事实并非如此。 对于适合一个寄存器的变量,编译器选择RAXRDX假设另一个未修改 ,就像"=r"约束选择一个寄存器并假设其余寄存器未经修改。 或者"=Q"约束选择a,b,c或d中的一个。 (参见x86约束 )。

在x86-64中,对于unsigned __int128操作数,通常只需要"=A" ,如多个结果或div输入。 这是一种黑客攻击,因为在asm模板中使用%0只会扩展到低位寄存器,并且当"=A" 使用ad寄存器时没有警告。

要确切了解这是如何导致问题的,我在asm模板中添加了注释:
__asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t)); 。 所以我们可以看到编译器期望的内容,基于我们用操作数告诉它的内容。

生成的循环(在Intel语法中)看起来像这样,通过在Godbolt编译器资源管理器上编译 64位gcc和32位clang的代码的清理版本:

 # the main loop from gcc -O3 targeting x86-64, my comments added .L6: rdtsc # compiler picked rax # c1 = rax rdtsc # compiler picked rdx # c2 = rdx, not realizing that rdtsc clobbers rax(c1) # compiler thinks RAX=c1, RDX=c2 # actual situation: RAX=low half of c2, RDX=high half of c2 sub edx, eax # tsccost = edx-eax js .L3 # jump if the sign-bit is set in tsccost ... rest of loop back to .L6 

当编译器计算c2-c1 ,它实际上是从第二个rdtsc计算hi-lo因为我们向编译器说明了asm语句的作用。 第二个rdtsc破坏了c1

我们告诉它它可以选择哪个寄存器来输出,所以它第一次选择一个寄存器,另一个第二次选择,所以它不需要任何mov指令。

TSC计算自上次重启以来的参考周期。 但是代码并不依赖于hi ,它只取决于hi-lo的符号。 由于lo每隔一两圈(2 ^ 32 Hz接近4.3GHz),所以在任何给定时间运行程序有大约50%的机会看到负面结果。

它不依赖于hi的当前值; 在一个方向或另一个方向上可能存在2^32偏差中的1个部分,因为当lo环绕时, hi变为1。

由于hi-lo是几乎均匀分布的32位整数,因此平均值的溢出非常普遍。 如果平均值通常很小,那么您的代码就可以了。 (但是看看其他答案为什么你不想要平均值;你想要中位数或什么来排除exception值。)

我的问题的主要问题不在于结果的准确性,而在于我偶尔得到负值(第一次调用rdstc比第二次调用更有价值)。 我做了更多的研究(并在本网站上阅读其他问题),我发现在使用rdtsc时让一些东西工作的方法是在它之前放置一个cpuid命令。 此命令序列化代码。 这就是我现在正在做的事情:

 static inline uint64_t get_cycles() { uint64_t t; volatile int dont_remove __attribute__((unused)); unsigned tmp; __asm volatile ("cpuid" : "=a"(tmp), "=b"(tmp), "=c"(tmp), "=d"(tmp) : "a" (0)); dont_remove = tmp; __asm volatile ("rdtsc" : "=A"(t)); return t; } 

我仍然在get_cycles函数的第二次调用和第一次调用之间得到一个负面的区别。 为什么? 我不是100%肯定cpuid程序集内联代码的语法,这是我在互联网上找到的。

面对热量和空闲节流,鼠标移动和网络流量中断,无论它在GPU上做什么,以及现代多核系统可以在没有任何人关心的情况下吸收的所有其他开销,我认为你唯一合理的做法是积累几千个单独的样本,然后在取中位数或均值之前抛弃exception值(不是统计学家,但我敢冒险,这里没有太大的区别)。

我认为你所做的任何事情都可以消除正在运行的系统的噪音,这会使结果偏差,而不仅仅是接受你无法可靠地预测这些天需要多长时间完成任务。

rdtsc可用于获得可靠且非常精确的经过时间。 如果使用linux,您可以通过查看/ proc / cpuinfo来查看您的处理器是否支持恒定速率tsc,以查看是否定义了constant_tsc。

确保你保持相同的核心。 每个核心都有自己的tsc,它有自己的价值。 要使用rdtsc,请确保使用taskset ,或SetThreadAffinityMask (windows)或pthread_setaffinity_np来确保您的进程保持在同一核心上。

然后你用你的主时钟速率除以linux上的主时钟速率可以在/ proc / cpuinfo中找到,或者你可以在运行时通过

RDTSC
clock_gettime
睡1秒钟
clock_gettime
RDTSC

然后查看每秒有多少刻度,然后你可以划分任何刻度差异以找出已经过了多少时间。

如果运行代码的线程在核心之间移动,那么返回的rdtsc值可能小于在另一个核心上读取的值。 当封装上电时,内核并非都将计数器设置为0。 因此,请确保在运行测试时将线程关联性设置为特定的核心。

我在我的机器上测试了你的代码,我认为在RDTSCfunction期间只有uint32_t是合理的。

我在我的代码中执行以下操作来纠正它:

 if(before_t