20

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

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

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

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

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


(编者注:请参阅Get CPU cycle count?获取获取完整 64 位时间戳的正确且可移植的方法。"=A"当为 x86-64 编译时,asm 约束只会获取低 32 位或高 32 位,具体取决于寄存器分配是否发生选择 RAX 或 RDX 作为uint64_t输出。它不会选择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;
}
4

9 回答 9

59

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

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

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

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

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

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

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

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

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

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

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

当然,不同的 CPU 有自己的 TSC。这意味着如果应用程序直接使用 TSC,它们会在不同的 CPU 上获得不同的值。帮助人们解决操作系统无法解决问题的问题(通过像他们应该的那样模拟 RDTSC);AMD 添加了RDTSCP返回 TSC 和“处理器 ID”的指令(英特尔最终也采用了该RDTSCP指令)。在损坏的操作系统上运行的应用程序可以使用“处理器 ID”来检测它们何时在与上次不同的 CPU 上运行;并且通过这种方式(使用RDTSCP指令),他们可以知道“elapsed = TSC - previous_TSC”何时给出有效结果。然而; 此指令返回的“处理器 ID”只是 MSR 中的一个值,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),时间戳计数器“随着每个内部处理器时钟周期递增”并且“

于 2013-11-13T00:21:05.937 回答
7
  1. 不要使用平均值

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

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

    在此处输入图像描述

  2. 无需测量开销RDTSC

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

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

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

于 2014-02-05T09:07:11.353 回答
3

If you code starts off on one processor then swaps to another, the timestamp difference may be negative due to processors sleeping etc.

Try setting the processor affinity before you start measuring.

I can't see if you are running under Windows or Linux from the question, so I'll answer for both.

Windows:

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

Linux:

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity (getpid(), sizeof(cpuset), &cpuset)
于 2014-02-05T11:09:16.350 回答
2

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

另一种可能性是您将其编译为 32 位代码,但重复次数更多,并且在没有固定 TSC(所有内核同步 TSC)的系统上偶尔会出现 CPU 迁移的负间隔。要么是多插槽系统,要么是较旧的多核。 CPU TSC 获取操作,尤其是在多核多处理器环境中


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

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

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


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

rdtsc(insn ref 手动条目) 总是在hi:lo中产生其 64 位结果的两个 32 位一半edx:eax,即使在 64 位模式下,我们实际上更愿意将它放在单个 64 位寄存器中。

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

在 x86-64 中,您通常只需要"=A"一个unsigned __int128操作数,例如多重结果或div输入。这是一种 hack,因为%0在 asm 模板中使用只会扩展到低位寄存器,并且在"=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时,它实际上是hi-lo从 2nd 开始计算的rdtsc因为我们在 asm 语句的作用上对编译器撒了谎。第 2 次rdtsc被击溃c1

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

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

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

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

于 2018-08-19T08:51:35.080 回答
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 汇编内联代码的语法,这是我在网上找到的。

于 2013-11-13T18:37:21.017 回答
0

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

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

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

rdtsc
clock_gettime
休眠 1 秒
clock_gettime
rdtsc

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

于 2013-12-01T00:58:20.500 回答
0

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

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

于 2013-11-12T23:34:11.410 回答
0

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

于 2014-03-18T20:41:44.803 回答
0

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

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

if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}
于 2015-06-30T08:36:06.893 回答