使用rdtsc获取细粒度程序动态执行时间

来源:互联网 发布:linux 增加定时任务 编辑:程序博客网 时间:2024/04/28 15:56

前一段为了验证基于PMU的CPU Cycle Profile的精确性,做了一系列实验。目前实验已经告一段落,现将实验心得记下来,与大家分享,并为将来的工作参考。

 

背景:

现代CPU上都有一块逻辑部件叫做Performance Monitoring Unit(PMU)。其工作原理很简单,就是一个简单的计数器。用户可以指定某个特定的事件(Event)。每当这个指定的事件发生时,计数器就会加1。当计数器发生溢出时,会产生系统中断,操作系统可以在中断处理函数中将当前引起中断的指令地址记下。这样就可以实现基于采样的Profile。

 

PMU的功能很强大,它为用户提供了一百多种事件。例如,当我们指定Instruction Retired事件时,我们得到的profile理论上应该反映程序每条指令执行的频度;而当我们指定CPU Cycle事件时,我们得到的profile理论上应该反映程序动态执行时间的分布。好的,有了这两个profile,我们就可以直观的计算出每条指令的CPI,并用这个参数来指导程序性能的优化。通过PMU来得到这两种profile的好处非常明显,就是对程序的性能影响非常小。

 

然而,由于现代处理器相当复杂,尤其加入了乱序执行的因素,通过上面两个参数得到的profile往往并不精确。我们希望做的一件事情,就是衡量一下这两个profile到底有多么不精确。

 

要验证Instruction Retired事件的profile的精确性非常简单。我们只需要对程序进行插装(用pin等工具),从而得到每条指令真实的执行次数。这样,我们就可以将PMU得到的profile与插装得到的标准答案进行对比。然而,要验证CPU Cycle的精确性,我们直接面临的问题就是:怎样才能得到标准答案呢?简单想起来,有两种途径:

1、通过模拟器。这要求模拟器在每个周期都是精确的。这样的模拟器有没有呢?答案是肯定的,但是我们肯定无法得到,因为只有Intel公司内部才可能有所有电路逻辑的实现细节。

2、通过在程序中插装,动态记录每个程序段执行的时间。

 

第二个方法看起来并不难,于是立即开始做。没想到一做两个星期就耗进去了。碰到的具体问题还是挺复杂的。本文将对这些细节问题进行总结。至于更详细的背景介绍,我们做这个工作的动机,以及后续工作的细节,将在另一篇论文中讨论。

 

目的:

通过向程序中插入代码,获取每个函数在程序中执行的时间。

之所以将粒度定位在函数级别,是因为这项工作本身就有很大的Overhead,如果在基本块(Basic Block)级别来做,将带来更大的多的负担,而使程序的行为大幅度改变。

 

方法:

在每个函数的入口和出口插入计时指令rdtsc,同时,维护一个全局数组来记录每个函数执行的时间。

 

遇到的问题及解决方法:

问题1:用什么工具来插装

可以选择动态插装和静态插装。考虑到动态插装的Overhead太高,可能改变程序行为,故采用静态插装。对于插装工具的选择,本打算采用二进制插装工具ATOM,结果发现这个工具已经老到网上都下不到了。。。寻觅了至少一整天,最终决定用Open64编译器进行插装。具体的作法,开始采用的是在高级中间表示(IR)上进行插装,结果发现编译器后端会在我插入的代码后加入一些代码,如寄存器的spill/fill。于是直接改代码生成部分,直接输出汇编指令。

 

问题2:插装部分以什么形式存在

一开始从简单的角度出发,决定自己将处理的部分写成一个library,在CG里只要插入一条function call就可以了。结果实验表明有很大一部分时间是耗费在library里面的;而library只有一个,没法对于每个函数单独统计。于是后期花了一些功夫直接将二进制代码在CG里面输出出来。这样每个函数都有自己的记录时间的部分;而记录时间的部分也被算入了宿主函数的执行时间。

 

问题3:rdtsc的原子性

这个问题从最初一直困扰我,知道第二周我才发现起原因。问题是这样,rdtsc的输出是两个寄存器,eax(存当前cycle的低32位)和edx(存当前cycle的高32位),也就是说,当eax为0xffffffff时,下一个cycle,edx的值应该加1。然而,我实验观察到的结果是,当eax溢出时,edx并不是马上加1,而是有一个延迟。。。于是,我在记录时间时,竟然发现有时间会倒流。。。

这个问题的解决方法,就是只记录eax的值,而忽略edx的值。也就是说,所有的计算(加法和减法),都只进行低32位的操作。我大致证明了这种做法的正确性,但是,这么做的前提是,每个函数的执行时间不能大于2的32次方。这个假设看似合理,但是在spec2000中的300.twolf中我就发现了一个反例。有个函数uloop就有一个执行了超久的Loop,直接导致我得到错误的时间数据。但是没有办法,只能用这样近似的估算了,除非Intel在新版CPU将rdtsc直接写入一个64位寄存器,或者保证其操作的原子性。。。

 

问题4:插装的正确性

仅在程序中插入rdtsc是不够的,还必须要有若干条指令来将返回的值记录回内存中。对于每个程序的入口,我们很容易处理,因为我们可以将rdtsc放在第一条指令,而后面指令的执行时间自然会被记录进来。然而,在程序出口的地方,rdtsc指令之后的指令应该都将被错误地记为caller的执行时间。对于大的函数,这些误差可以忽略不计。但是对于小的函数,这些误差的影响就很严重了。

这个问题目前的解决方法是,我通过一个微型测试程序获取误差的的大小(大约为23个cycle),并在插装程序中进行修正(即在出口函数调用完rdtsc后,将eax的值加上23)

原创粉丝点击