与计时器和cpuid有趣

2021-03-16 17:41:28

这一次,我将在M1上的X86_64仿真呈现自身的X86_64仿真方式中查看高分辨率定时器和几个奇怪,导致一些潜在的“Gotchas”。

我创建了一个小程序来展示我在这里讨论的问题,您可以下载以便运行自己。它是名义职业.CC,可以编译,没有定义,标志或您最喜欢的C ++编译器为AARCH64或X86_64(例如Clang ++名称职权FRequency.cc)。代码有效地用于SRC / stats-timing.cc中的LOMP实现中,它本身由LOMP Microbm目录中的微基准使用。

对于微基准,具有高分辨率,低开销定时器,理想的是,我们可以在单个指令中访问的很有用。虽然最便携的事情是使用std :: chrono :: stiby_clock(按照建议避免std :: chrono :: high_resolution_clock),我们可以通过调用进入运行时库来实现它所以没有 - 激动人心的开销(它将显着影响寄存器分配等),因此如果我们可以,它值得直接到硬件。

在我们查看定时器实现之前,让我们考虑我们想要的属性:

低干扰:插入代码读取计时器不会大幅度更改代码的执行。

同步:它在不同的逻辑CPU之间同步(因此我们可以在不同线程中的时间点之间取消差异并获得明确的时间)。

如果我们可以访问单个指令来读取定时器,可以帮助干扰和开销,尽管我们仍然必须小心1。

在AARCH64中,我们可以使用MRS指令读取包含计时器计数器的CNTVCT_EL0系统寄存器,以及读取CNTFREQ_EL0寄存器,该寄存器告诉我们计数器增量的频率。

//设置函数,我们需要访问高分辨率//时钟#define generate_read_system_register(结果类型,funcname,reg)\内联resultype funcname(){\ uint64_t res; \ __asm_ volatile("夫人\ t%0," #reg:" = r"(res)); \ return res; generate_read_system_register(uint64_t,readcyclecount,cntvct_el0)generate_read_system_register(uint32_t,gethrfreq,cntfrq_el0)#undef generate_read_system_register

除了这个定时器蜱虫似乎相对较低的问题(频率通常在1MHz至50MHz范围内)的速度之外。(“),这一切都很容易;时钟是单调,它在所有可用逻辑CPU中都很常见,我们可以轻松阅读时钟,并且还可以轻松发现它刻度的速度。

这里的东西有点“乐趣”。读取计时器仍然是单个指令(RDTSC),但获取其属性更难,其属性随着时间的推移而发展。

在第一实施方式中,计数器计数为“CPU时钟”,然而,由于CPU时钟速率可以由电源管理系统向上和向下移动,这意味着它没有测量,壁钟,时间。几年前发生了变化,但我们必须检查在使用挂钟时间测量的计时器之前正在运行的CPU上是否在其运行的CPU上实现了该更改。

也没有简单,普遍商定,方法可以找出即使在不变时计时器递增的速率。

执行这些检查并尝试提取CPU的属性将我们带到了CPUID的乐趣。

CPUID是用于获取有关X86_64(或IA32),CPU实现的信息的信息的指令。不幸的是,尽管大多数供应商实施了指令,但如何在它们之间使用它的细节。有用的文档如果您希望在这里更深入是英特尔软件开发人员手册(第2卷有CPUID指令的条目),以及AMD64架构程序员手册(见第3卷的附录D和E)3。

供应商都提供了一种兼容的方法来发现您正在使用的供应商的CPU,以便您可以选择使用CPUID发现更多信息的合适方法,并且至少AMD *和Intel *,可以设法进行一些常见接口。

这是一些简单的代码,可以为我们提供对CPUID的低级访问。这也可以通过编译器内在所可行,但该ASM代码至少适用于GCC和LLVM,至少。

/ * CPUID乐趣。在这里,我们需要检查*时间戳计数器的理智。 * / struct cpuid_t {uint32_t eax; UINT32_T EBX; UINT32_T ECX; uint32_t edx;};静态内联void x86_cpuid(int叶,int sublaf,struct cpuid_t * p){__asm__ __volatile __(" cpuid":" = a"(p-> eax) ," = b"(p-> ebx)," = c"(p-> ecx)," = d"(p-> edx ):" a"(叶)," c"(subleaf));}

static std :: string cpubrandname(){cpuid_t cpuinfo; UINT32_T INTBUFFER [4]; char * buffer =(char *)& intbuffer [0]; //所有x86供应商都同意这片叶子。 //,你在这里阅读的内容然后确定你的//应该如何解释其他叶子。 x86_cpuid(0x00000000,0,& cpuinfo); Intbuffer [0] = CPUInfo.ebx; intbuffer [1] = cpuinfo.edx; Intbuffer [2] = cpuinfo.ecx;缓冲器[12] = char(0);返回缓冲区;}

AMD和英特尔都使用CPUID LEAD 80000007H EDX位8告诉我们TSC时钟是否测量时间(不变)或CPU时钟滴答。当然,旧的处理器甚至可能甚至不支持这片叶子,所以我们必须先检查一下!

静态BOOL HAVARINVARIANTTSC(){//这些叶子对英特尔和AMD很常见。 cpuid_t cpuinfo; //可以告诉我们存在的叶子吗? x86_cpuid(0x80000000,0,& cpuinfo); if(cpuinfo.eax< 0x80000007){//这个处理器甚至无法告诉我们它是否有Invarianttsc!返回false;至少CPU可以告诉我们它是否支持//不变性的TSC。 x86_cpuid(0x80000007,0,& cpuinfo);返回(cpuinfo.edx&(1< 8))!= 0;}

我们已经了解了如何发现它是否有理由使用RDTSC进行经过时间,但我们还不知道每个刻度所代表的时间。由于它表明“1”意味着“1S”或“1ns”,我们需要找到它。

英特尔指定了一个CPUID叶子,使我们提供该信息(叶15H),但它们最近只有这么做,而且我尚未看到一个实现这一点的CPU。 (代码检查它,如果它可以使用它,但它显然不是一般的解决方案,显然尚未测试:-))。

虽然没有规范,所以需要与名义时记柜台速率相同,但到目前为止我还没有看到它不同的英特尔处理器。

AMD似乎没有任何方法可以通过CPUID找到这一点(他们在模型名称字符串中编码它,他们更喜欢吹嘘核心的数量,而不是时钟速率),所以我们所能做的就是通过比较我们从RDTSC与另一个我们信任的计时器(即std :: chrono :: stiefd_clock)来解决这个问题。

那么,你会期待什么?这里没有英特尔硅,所以它应该展示什么品牌?

这略有意外,但你可以看到为什么苹果想要声称是刚内蒙特,因为x86_64麦克斯的现有代码很可能只是知道如何解码英特尔的CPUID接口,因为这就是它可能已经看到的全部。随着仿真的整个点是支持这种代码而无需改变,苹果仿真希望显示该代码所期望的代码。

与真正的英特尔实现一样,它也告诉我们标称时钟率,所以我们都完成了,对吧?我们现有的计时器代码可以使用它,它会将所有“正常工作”。在我回答这一点之前,让我们来看看我们在原生AARCH64环境中看到的内容。

正如我们在上面看到的那样,在这里获取有关计时器的信息很简单,我们看到了这一点: -

这似乎完全是合理的,但使仿效X86_64环境看起来很可疑。在这里,我们有一个〜42ns 4的单位,但我们有1 / 2.5GHz = 400pps中的一个。由于仿效环境似乎不太可能访问比底层硬件更高的分辨率时钟,因此我们看到的是奇怪的。

我们可以通过将其与STD :: Chrono :: stible_clock进行比较来检查RDTSC时钟的单位而不是信任我们获得的信息,而不是相信RDTSC时钟的单位(正如我们在AMD上的那样)。

品牌:正版模型:VirtualApple @ 2.50GHz ...... Sanity Check反对STD :: Chrono :: stiefy_clock提供频率999.98 MHz => 1.00 NS.

所以......虽然品牌名称是刚内蒙特,但RDTSC时钟单元不是我们期望从型号名称中的标称CPU频率。而且,如果我们认为我们测量的时期将是25倍太小!

但是,这并非全部。甚至1ns甚至远小于底层硬件使用的41.67ns单位。

我们实际上看到的是测量时钟时间的单位与它刻度的速率不同,因此时钟的每个变化都不是一个刻度,但数量更大。

我们可以尝试使用这样的代码来解决方法,以查看滴答数的变化,我们可以看到: -

//尝试查看时钟是否以相同的速率//以相同的速率//作为其值枚举。考虑一个时钟,其中值//以秒为单位枚举,但其中仅更改一次//小时... //只是因为时钟有一个精细的间隔,那么它可以解决它可以解析为该级别.Static uint64_t measureclanular(){//如果时钟非常慢,这可能无法正常工作... UInt64_t delta = std :: numeric_limits< uint64_t> :: max(); for(int i = 0; i< 50; i ++){uint64_t m1 = readcyclecount(); uint64_t m2 = readcyclecount(); uint64_t m3 = readcyclecount(); uint64_t m4 = readcyclecount(); uint64_t m5 = readcyclecount(); uint64_t m6 = readcyclecount(); uint64_t m7 = readcyclecount(); uint64_t m8 = readcyclecount(); uint64_t m9 = readcyclecount(); uint64_t m10 = readcyclecount();自动d =(m2 - m1); if(d!= 0)delta = std :: min(d,delta); d =(m3 - m2); if(d!= 0)delta = std :: min(d,delta); d =(m4 - m3); if(d!= 0)delta = std :: min(d,delta); ......所选择的代码保持这个例子可以管理... ...它计算了同样的方式...} return delta;}

X86_64处理器:品牌:正版模型:VirtualApple @ 2.50GHz不变TSC:真正的CPUID叶片15H不支持测量频率999.13 MHz => 1.00 NS Sanity Check针对STD :: Chrono :: stive_clock提供频率999.98 MHz => 1.00 ns测量粒度= 41滴答=> 24.37 MHz,41.04 ns

这向我们展示了虽然测量时间是NS的单位,但是时钟只能以最佳的41ns解析,这与我们在AARCH64侧看到的底层硬件时钟对齐: -

AARCH64处理器:来自高分辨率定时器频率(CNTFRQ_EL0)24.00 MHz => 41.67 NS Sanity Check反对STD :: Chrono :: stive_clock提供频率23.90 MHz => 41.85 ns测量粒度= 1滴定=> 24.00 MHz,41.67 NS

我们还可以在各种其他X86_64处理器上运行代码,看看他们做了什么......

X86_64处理器:品牌:正版模型:英特尔(R)Xeon(R)金6230 CPU @ 2.10GHz不变TSC:真正的CPUID叶15H不给频率频率为2.10 GHz => 476.19 PS Sanity Check反对STD :: Chrono :: stive_clock提供频率2.09 GHz => 477.33 PS测量粒度= 60滴度=> 35.00 MHz,28.57 ns

X86_64处理器:品牌:正版模型:英特尔(r)Xeon(r)CPU E5-2695 V4 @ 2.10GHz不变性TSC:型号名称串频率2.10 GHz =&GT不支持真正的CPUID叶15H; 476.19 PS Sanity Check针对STD :: Chrono :: stiby_clock提供频率2.10 GHz => 477.29 PS测量粒度= 42滴度=> 50.00 MHz,20.00 ns

X86_64处理器:品牌:Authenticamd型号:AMD EPYC 7742 64核处理器不变TSC:Requiration CPUID叶15H不支持测量频率2.25 GHz => 444.46 PS Sanity Check反对STD :: Chrono :: stive_clock提供频率2.25 GHz => 444.43 PS测量粒度= 22滴答=> 102.27 MHz,9.78 ns

这向我们展示了,正如我们所预期的,我们测试的所有英特尔处理器都使用与其型号名称中的标称频率相同的单位,但在所有X86_64架构中的实际分辨率低于测量它的单位。因此,虽然M1时钟的分辨率是最低的,但它不在第一次出现的情况下遥远。

如果您在M1上的模拟X86_64环境中使用RDTSC测量时间,请非常小心。你看到的时间可能比现实小2.5倍!

M1上的X86_64仿真可以在未指定硬件行为的地方误导您。

即使在不考虑超出无序的复杂性,计时器也比你可能合理预期的更复杂。

我是用m1的东西完成(直到别的东西咬我),所以可能是一些影响广播,障碍物,锁等的各种机器的内存行为。

1我不会进入究竟意味着在无序的处理器的指令流中插入计时器的内容,因为它略有偏离主题,并且讨论它使这个博客太长了。 2这方面是由ARM固定的; 它们具有更新的规范,该规范将频率设置为1GHz。 (请参阅ARM A-Profile架构中的开发:ARMv8.6-A)。 3我无法在HTML中找到这些体系结构文档,就像PDF一样难以引用。 4这42个似乎不太可能是“生命的答案,宇宙和一切”,但也许是! 5这是在“书”中,但有一些较新的机器可以衡量。