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

时间:2013-11-12 22:49:46

标签: c x86-64 inline-assembly overhead rdtsc

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

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

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

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

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


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

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


#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

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;
}

9 个答案:

答案 0 :(得分:51)

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

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

英特尔跟随AMD并改变了他们的TSC的行为以测量时间,并且还采用了AMD的TSC Invariant&#34;标志。

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

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

  • TSC衡量绩效而不是时间

  • TSC衡量的是时间而非绩效,但并未使用&#34; TSC Invariant&#34;要说出来的标志

  • TSC衡量的是时间而非绩效,并使用&#34; TSC Invariant&#34;标志这么说(最现代的CPU)

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

非常不切实际

另请注意,如果TSC确实测量时间,则无法使用其他时间源确定时间,您无法知道它返回的时间刻度(&#34;假装周期&#34;中有多少纳秒)比例因子。

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

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

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

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

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

最后,如果你真的想要正确地做,你应该测量测量的开销。要做到这一点,你要测量什么都不做的时间(仅仅是RDTSC / RDTSCP指令,同时丢弃不正确的测量值);然后从&#34;测量某些东西&#34;中减去测量的开销。结果。这样可以更好地估计时间&#34;某些事情&#34;实际上需要。

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

答案 1 :(得分:6)

  1. 不使用平均值

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

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

    enter image description here

  2. 无需衡量RDTSC

    的开销

    你只需要在一段时间内测量一下,两次都会出现相同的偏移量,减去后它就会消失。

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

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

答案 2 :(得分:3)

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

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

我无法看到你是在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)

答案 3 :(得分:2)

其他答案很好(请阅读它们),但是假设rdtsc被正确读取。这个答案正在解决导致完全伪造结果(包括否定结果)的inline-asm错误。

另一种可能性是您将其编译为32位代码,但重复的次数更多,并且在没有不变TSC(跨所有内核同步的TSC)的系统上,CPU迁移有时会出现负间隔)。多插槽系统或较旧的多核系统。 CPU TSC fetch operation especially in multicore-multi-processor environment


如果要针对x86-64进行编译,则您对"=A"的不正确的asm输出约束将完全解释您的负面结果。请参见Get CPU cycle count?以获取正确信息所有编译器均可移植的rdtsc的使用方式以及32位和64位模式。或使用"=a""=d"输出,只是忽略高半部分输出,这样就不会出现32位溢出的短间隔。)

(令我惊讶的是,您没有提到它们也很大,并且变化很大,即使没有单独的测量结果为负,也溢出tot来给出负平均值。我看到的平均值是-6342189969374170115365476。)

使用gcc -O3 -m32进行编译可以使其按预期方式工作,平均输出24到26(如果以循环方式运行,则CPU保持最高速度,否则像125个参考周期一样,在返回之间间隔24个核心时钟周期)到rdtsc在Skylake上)。 https://agner.org/optimize/用于说明表。


"=A"约束出了什么问题的Asm详细信息

rdtsc (insn ref manual entry) 始终会在hi:lo中产生64位结果的两个32位edx:eax的一半,即使在64位模式下,确实希望将其存储在单个64位寄存器中。

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

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

要确切了解这是如何引起问题的,我在asm模板中添加了一条注释:
__asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t));。这样,我们便可以根据对操作数的了解,看到编译器的期望。

通过为64位gcc和32位clang编译代码on the Godbolt compiler explorer的清理版本,结果循环(采用Intel语法)如下所示:

# 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时,实际上是从第二个hi-lo开始计算rdtsc,因为我们撒谎了编译器有关asm语句的作用。第二个rdtsc毁了c1

我们告诉它,它选择要输出的寄存器,因此它第一次选择一个寄存器,第二次选择另一个寄存器,因此它不需要任何mov指令。

TSC计算自上次重新引导以来的参考周期。但是代码不依赖于hi<lo,而仅依赖于hi-lo的符号。由于lo每隔一两秒就会回绕一次(2 ^ 32 Hz接近4.3GHz),因此在任何给定时间运行程序大约有50%的机会看到负面结果。

它不取决于hi的当前值; 2^32可能在一个方向或另一方向上有1个部分,因为hilo环绕时会改变一个。

由于hi-lo是几乎均匀分布的32位整数,因此平均值的溢出非常常见。如果平均值通常很小,则您的代码正常。 (但请查看其他答案,以了解为什么您不想要平均值;您想要中值或排除异常值。)

答案 4 :(得分:1)

我的问题的主要观点不是结果的准确性,而是我偶尔得到负值的事实(第一次调用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程序集内联代码的语法,这是我在互联网上找到的内容。

答案 5 :(得分:0)

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

我认为你所采取的任何措施都可以消除正在运行的系统的噪音,这会使结果偏差,而不仅仅是接受你无法预测多长时间这些天它将任何完成。

答案 6 :(得分:0)

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

确保您保持相同的核心。每个核心都有自己的tsc,它有自己的价值。要使用rdtsc,请确保tasksetSetThreadAffinityMask(窗口)或pthread_setaffinity_np以确保您的流程保持在同一核心。

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

来完成

RDTSC
clock_gettime
睡1秒钟 clock_gettime
RDTSC

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

答案 7 :(得分:0)

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

答案 8 :(得分:0)

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

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

if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}