我正在编写一个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;
}
答案 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)
不使用平均值
使用最小的一个或平均较小的值(由于CACHE而获得平均值),因为较大的值已被OS多任务处理中断。
您还可以记住所有值,然后找到操作系统进程粒度边界并过滤掉此边界之后的所有值(通常&gt; 1ms
,这很容易检测到)
无需衡量RDTSC
你只需要在一段时间内测量一下,两次都会出现相同的偏移量,减去后它就会消失。
适用于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
来给出负平均值。我看到的平均值是-63421899
或69374170
或115365476
。)
使用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
。但这不是事实。 对于适合一个寄存器的变量,编译器选择RAX
或RDX
并假定另一个未被修改 ,就像"=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个部分,因为hi
在lo
环绕时会改变一个。
由于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,请确保taskset或SetThreadAffinityMask(窗口)或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;}