【问题标题】:Negative clock cycle measurements with back-to-back rdtsc?使用背靠背 rdtsc 进行负时钟周期测量?
【发布时间】:2013-11-25 08:01:47
【问题描述】:

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

首先使用平均值是否正确?

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

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

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


(编者注:有关获取完整 64 位时间戳的正确且可移植的方法,请参阅 Get CPU cycle count?"=A" asm 约束在为 x86-64 编译时只会获得低 32 位或高 32 位,具体取决于寄存器分配是否恰好为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;
}

【问题讨论】:

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


【解决方案1】:

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

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

【讨论】:

  • 您不会丢弃异常值,您只需取许多 1000 次运行中的最低值。这是正确的。
【解决方案2】:

当英特尔首次发明 TSC 时,它测量的是 CPU 周期。由于各种电源管理功能,“每秒周期数”不是恒定的;所以 TSC 原本是用来衡量代码性能的(而不是用来衡量经过的时间)。

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

Intel 效仿 AMD,改变了 TSC 的行为来测量时间,并且还采用了 AMD 的“TSC Invariant”标志。

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

  • TSC 测量时间和性能(每秒周期数是恒定的)

  • TSC 衡量的是性能而不是时间

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

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

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

另请注意,如果 TSC 确实测量时间,如果不使用其他时间源来确定比例因子,您将无法知道它返回的时间尺度(“假装周期”中有多少纳秒)。

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

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

基本上; CPU是否支持RDTSCP指令,并且操作系统是否正确设置了“处理器ID”(使用MSR);那么RDTSCP 指令可以帮助应用程序知道它们何时会得到一个糟糕的“经过时间”结果(但它并没有提供任何修复或避免错误结果的方法)。

所以;长话短说,如果你想要一个准确的绩效衡量标准,那你就完蛋了。您实际上可以期望的最好的结果是准确的时间测量;但仅在某些情况下(例如,在单 CPU 机器上运行或“固定”到特定 CPU 时;或者在设置正确的操作系统上使用RDTSCP 时,只要您检测并丢弃无效值)。

当然,即使那样,由于 IRQ 之类的原因,您也会得到不可靠的测量结果。为此原因;最好在循环中多次运行您的代码,并丢弃任何高于其他结果的结果。

最后,如果你真的想正确地做,你应该测量测量的开销。为此,您需要测量什么都不做需要多长时间(仅 RDTSC/RDTSCP 指令,同时丢弃不可靠的测量值);然后从“测量某些东西”的结果中减去测量的开销。这可以让您更好地估计“某事”实际花费的时间。

注意:如果您可以从 Pentium 首次发布时(1990 年代中期 - 不确定它是否可以再在线获得 - 我从 1980 年代起存档了副本)时找到了一份英特尔的系统编程指南,您会发现英特尔将时间戳计数器记录为“可用于监视和识别处理器事件发生的相对时间”的东西。他们保证(不包括 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 发布日期)。

【讨论】:

  • 布伦丹,很好的答案。你能添加一些引用吗?
  • @Brendan:事实上,在现代英特尔处理器中,无论时钟频率、电源状态或正在使用的内核如何,TSC 都应该以相同的频率计数。
  • 我换一种说法:AMD 和 Intel 意识到高精度低开销时间源比循环计数器更有用。在现代 CPU 中,硬件性能计数器可以做到这一点,因此您不需要rdtsc。您可以测量除周期以外的事件以进行微基准测试。另请注意,一些早期的恒定速率 TSC CPU 在运行 hlt 指令时会停止 TSC,使其无法用作时间源。 (Linux 的 /proc/cpuinfo 显示 nonstop_tsc 表示没有此问题的 CPU,constant_tsc 表示固定速率特性。)
  • @MaximEgorushkin:我假设他们注意到 TS 代表时间戳(并且 TSC 不代表时间计数器)。请注意,可以在软件中实现单调递增的时间戳,例如(例如)mov eax,1; lock xadd [globalTimeStamp],eax,而不考虑“挂钟时间”。
  • 即使事实正确,答案中的观点也不正确。拥有这个计时器并不是没有用的。对于现在的仿真器来说,它非常有价值,因为它可以拥有一个纳秒精度的时钟,并且结果返回的延迟非常低。 PS。此外,自 2013 年以来,没有任何 CPU 会这样做,因此如果您的受众需要快速 CPU,则没有理由不认为它是一种可靠的方法。
【解决方案3】:

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

【讨论】:

  • 如果您正在为 x86-64 进行编译,可能是第一个 rdtsc 选择了 RAX,而第二个 rdtsc 选择了 RDX 作为输出,因为 "=A" 没有按照您的想法进行。 (所以你实际上是在踩到编译器的寄存器之后比较hi &lt; lolo &lt; hi。)
【解决方案4】:

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

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

然后你将它除以你的主时钟频率,它在 Linux 上可以在 /proc/cpuinfo 中找到,或者你可以在运行时通过

rdtsc
clock_gettime
睡一秒
clock_gettime
rdtsc

然后看看每秒有多少滴答声,然后你可以除以滴答声的差值来找出已经过去了多少时间。

【讨论】:

    【解决方案5】:
    1. 不要使用平均值

      改用最小的一个或较小值的平均值(因为 CACHE 得到平均值),因为较大的值已被操作系统多任务中断。

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

    2. 无需测量RDTSC的开销

      您只是测量偏移了一段时间,并且两次都存在相同的偏移量,并且在减法之后它就消失了。

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

      您应该通过一些稳定的密集计算循环将 CPU 的速度更改为最大值,通常几秒钟就足够了。你应该持续测量CPU频率,只有当它足够稳定时才开始测量你的东西。

    【讨论】:

      【解决方案6】:

      如果您的代码在一个处理器上开始,然后切换到另一个处理器,则时间戳差异可能会因处理器休眠等而为负数。

      在开始测量之前尝试设置处理器亲和性。

      我无法从问题中看出您是在 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)
      

      【讨论】:

      【解决方案7】:

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

      【讨论】:

      • tsc 通常在同一插槽的内核之间同步,并且通常可以在多个插槽上同步 (stackoverflow.com/questions/10921210 "在较新的 CPU (i7 Nehalem + IIRC) 上,TSC 会在所有内核之间同步,并且以恒定速率运行。 ... Intel .. 在多插槽主板上的内核和封装之间是同步的")。这可能是操作系统为了获取全局高分辨率时钟源而完成的。
      【解决方案8】:

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

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

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

      【讨论】:

        【解决方案9】:

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

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


        如果您针对 x86-64 进行编译,则您的负面结果完全可以通过您对 asm 的错误 "=A" 输出约束来解释。请参阅 Get CPU cycle count?,了解正确使用 rdtsc 的方法可移植到所有编译器和 32 与 64 位模式。或者使用"=a""=d" 输出并简单地忽略高半输出,用于不会溢出32位的短间隔。)

        (我很惊讶你没有提到它们也是 巨大 并且变化很大,以及溢出 tot 给出一个负平均值,即使没有单独的测量结果是负的。我我看到像 -6342189969374170115365476 这样的平均值。)

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


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

        rdtsc (insn ref manual entry) alwaysedx:eax 中生成其 64 位结果的两个 32 位 hi:lo 一半,即使在我们更愿意使用的 64 位模式下单个 64 位寄存器。

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

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

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

        生成的循环(采用 Intel 语法)如下所示,来自为 64 位 gcc 和 32 位 clang 编译代码的清理版本 on the Godbolt compiler explorer

        # 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&lt;lo,它只依赖于hi-lo的符号。由于lo 每隔一两秒循环一次(2^32 Hz 接近 4.3GHz),因此在任何给定时间运行程序都有大约 50% 的机会看到否定结果。

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-10-27
          • 1970-01-01
          • 2015-09-08
          • 2020-08-19
          • 2022-06-29
          • 2021-09-26
          • 2017-07-21
          相关资源
          最近更新 更多