代码改变世界

内存性能工具:Part 1 内存操作调优

2010-06-30 18:09  Robbin  阅读(2566)  评论(2编辑  收藏  举报

按:这是大牛Ulrich Drepper的大作<What Every Programmer should know about memory>中的Part 7:Memory Performance Tools的翻译,会一点点贴上来,希望能对大家有所帮助。翻译还有待雕琢,原作信息量很大,有的地方后面会再补上一些注释。另外,最新的Intel Core i7的变化没有体现在文中,有时间我会以注释方式补充进来。

原文链接:http://lwn.net/Articles/257209/

7 内存性能工具

有大量的工具可以帮助程序员了解程序对缓存和内存的使用情况。现代的处理器都提供了性能监视部件。但是一些事件(event)难以准确测量,这也为仿真(Simulation)提供了一定空间。在更高的层面上,有特殊的工具可以监视进程的执行情况。我们会介绍一组可用于多数Linux系统上的常用工具。

 

7.1 内存操作调优

剖析(profile)内存操作需要硬件提供帮助。虽然可以仅通过软件来获取一些信息,但这些信息要么粒度太粗,要么仅仅是一种仿真。关于仿真的例子会在7.2和7.5介绍。这里主要关注可测量的内存操作。

在Linux上可以使用oprofile来获取性能监视器提供的信息。Oprofile提供连续的调优能力,具体的可参考[continuous]。它提供统计性的,系统范围的结果,并使用一个简单易用的界面。Oprofile无疑是当前唯一可以观测处理器性能测量数据的工具。使用pfmon的Linux开发者也会不时被这里描述的功能所震惊的。

*译注:pfmon已经转移到http://perfmon2.sourceforge.net/

Oprofile提供的接口简单,小巧,又足够底层,即使是使用图形化的界面(来展示结果)。用户需要选择记录哪些处理器事件,这些在处理器的架构手册中都有描述,不过多数时候需要了解更多的背景知识才能理解它们。另一个问题是如何理解收集到的数据,性能测量计数器只是一个数值并且可以任意增长,对一个给定的计数器多高才算高呢?

*译注:Oprofile支持两种采样(example)模式,基于时间和基于事件,这里主要关注基于事件的情况。Oprofile分为两部分,一个是内核模块 (oprofile.ko) ,一个是用户空间的守护进程 (oprofiled) 。前者负责访问性能计数器或者注册基于时间采样的函数(使用 register_timer_hook注册,使时钟中断处理程序最后执行 profile_tick时可以访问),并将采样置于内核的缓冲区内。后者则在后台运行,负责从内核空间收集数据,写入文件。可以参考developerworks的一个教程:http://www.ibm.com/developerworks/cn/linux/l-oprof/index.html。同时,Oprofiel还有一款图形界面前端,http://projects.o-hand.com/oprofileui

一个不完整的答案是避免仅观测一个值,而是对比多次观测到的结果。处理器可以监视多个事件,这样就可以检视收集来的数据的比值变化。这就给出了漂亮的,可比较的结果。通常除数是执行的时间,时钟周期数或指令数。作为对程序性能的初步测量,对比观测到这些数据的比值是很有意义的。

Figure 7.1: Cycles per Instruction (Follow Random)

图7.1: 周期每指令 (随机Follow)

图7.1 显示了对于简单的随机"Follow"测试例,当使用不同大小的工作集情况下的周期每指令数(CPI)情况。多数Intel处理器上用于收集此信息的事件是CPI_CLK_UNHALTED和INST_RETIRED。如同名字所提示的那样,前者统计CPU的时钟周期数,后者统计指令数。我们得到一幅显示了我们访问每个元素需要的周期数的图。对于小的工作集比率大概是1.0甚至更低,这些度量是在Intel Core 2处理器观测得到的,它是多标量(multi-scalar)的并且可以同时执行多条指令。程序没有受限于内存带宽,比例理应低于1.0,不过在这种情况下1.0也已经足够好了。

*译注:多标量结构在1992年就已经提出,简单说CPU内有多条流水线,可以同时执行多条指令。

一旦L1d的容量不够存储工作集,CPI跳跃到3.0左右。要注意CPI比值是将因读取L2而导致的惩罚平摊到所有指令后的结果,而不仅仅是内存指令。使用访问每个元素所需周期数的数据,可以计算出访问每个元素需要多少指令。当L2缓存容量不够时,CPI比例会跳升到20以上。这是预期中的结果。

性能测量计数器可以给出关于处理器的运行情况的更进一步的观察。而这需要我们考虑更多处理器的实现细节。在这个文档中,我们关注缓存处理的细节,所以我们必须更多了解关于缓存的事件的细节。这些事件,它们的名字,它们测量什么,还有那些是处理器特定的。这些也正是oprofile使用的难点所在,和使用的简单用户界面无关;用户必须自己指出性能计数的细节所在。在第10节,我们会了解一些处理器的细节。

对于Core 2处理器需要关注的事件是L1D_REPL,DTLB_MISSES和L2_LINES_IN。后者可以度量所有未命中和因为指令而不是硬件的预取而导致的未命中。随机"Follow"测试的结果显示在图7.2中。

*译注:Core 2之后,Intel又推出了i3, i5, i7三款新品,i3可以看作i5的简化,i7是原生四核设计,而i5采用了Nehalem架构,具体信息建议参考Intel手册。

Figure 7.2: Measured Cache Misses (Follow Random)

图7.2: 缓存未命中测量 (随机Follow)

所有的比例都是由有效指令完成数目(INST_RETIRED)计算得来的。这意味着没有操作内存的指令也被考虑在内了,也就是说,实际上操作内存的指令导致的缓存未失效比应该高于图中所显示的。

L1d失效比的高点超过所有其它项是因为L2未命中也包含了L1d未命中,这是由于Intel处理器使用了内含式缓存(inclusive caches)的缘故。处理器包含32k L1d,所以正如预期的那样,我们看到L1d 失效比从0开始跳升到这个工作集大小左右(除了访问数据结构,还有其他在使用缓存,这意味着增长是从16k到32k这段开始的)。有趣的是,在工作集未超过64k大小时,可以看到硬件预取功能可以将失效比保持在1%左右。这之后,L1d的失效比就火箭般的增长了。

*译注:内含式缓存,就是指L2包含L1的全部内容,L3包含L2的全部内容,也就是说L1没有,则L2也没有。

L2失效比保持在0直到L2耗尽;很少的未命中是因为其它使用L2者没有很明显的影响L2的可用数量。一但L2的容量(221 字节)用光,失效比开始增长。很重要的是,L2请求失效比是非0的。这表明硬件预取器没有载入所有后续指令需要的缓存行。这正是所预期的,随机存取阻碍了完美的预取行为。可以拿这个和图7.3中顺序读取的数据进行比较。

Figure 7.3: Measured Cache Misses (Follow Sequential)

图7.3: 缓存未命中测量 (顺序Follow)

在这幅图中,我们可以看到L2请求失效比基本上是0(注意这幅图的变化曲线不同于图7.2)。对于顺序存取的情况,硬件的预取器功能表现得很完美;几乎所有的L2缓存未命中都是由于预取器造成的。L1d和L2缓存失效比基本相同的事实说明L1d缓存未命中都由L2缓存处理而没有造成进一步的延迟。这对所有程序来说都是最理想的情况,但实际上是很难达到的。

两幅图中的第四条线都是DTLB失效比(Intel分离了代码和数据的TLB,DTLB代表数据TLB)。对于随机存取的情况,DTLB失效比变化明显并且导致了延迟。有趣的是DTLB的惩罚要早于L2失效。在顺序存取的情况下DTLB的消耗基本为0。

回到6.2.1节的矩阵相乘的例子以及9.1节的示例代码,我们可以使用另外3种计数器。SSE_PRE_MISSS,SSE_PRE_EXEC和LOAD_HIT_PRE计数器可以用于观察软件预取的效率。9.1节中的代码运行后有如下结果:

DescriptionRatio
Useful NTA prefetches 2.84%
Late NTA prefetches 2.65%

较低的有效NTA预取(non-temporal aligned)比例表明许多预取指令运行取到的缓存行实际已经加载,也就是做了无用功。这意味处理器浪费了时间去解码预取指令及查找缓存。无法判断代码是否噪音太多。很大程度取决于处理器使用的缓存大小,硬件预取器也起到了一定作用。

较低的延迟NTA预取率则具有欺骗性。这个比值意味着2.65%的预取指令执行的太晚了。需要数据的指令已经在数据被预取到缓存之前执行完毕了。特别需要注意的是只有2.84%+2.65%=5.5%的预取指令是很有用的。而有效的NTA预取指令中,有48%没有及时完成。代码因此可以进一步优化:

  • 大部分预取指令是不需要的。
  • 预取指令的使用需要更好的配合硬件特性。

如何更好的使用可用硬件作为练习留给读者。具体的硬件规格起到决定作用。在Core 2处理器上SSE算术操作的延迟是1个时钟周期,而更早的版本需要2个周期的延迟,同时硬件预取器和预取指令需要更多的时间得到数据。

要决定什么时候需要预取-或者不需要-可以使用opannotate程序。它显示源码或汇编代码并指出哪条指令会引发事件。要注意有两点:

*译注:opannotate是OProfile的一个组件。

  1. Oprofile进行随机的调优。只会每隔N个事件进行记录(N是一个每事件的阈值且会有一个最小值)以避免使系统操作变得太慢。有可能某一行引发了100个事件但没有显示在报告中。
  2. 不是所有事件都会被准确记录。例如,在某些特殊事件被记录时,指令计数器的结果可能不正确。处理器的多标量化使得很难给出100%正确的结果。当然,某些事件在一些处理器上是准确的。

注释的信息不仅对了解预取信息有用。每个事件都会被指令指针寄存器所记录。因此也可以指出程序中的其它热点。定位代码中哪里INST_RETIRED事件执行得最频繁,然后对其进行调优。定位哪里缓存未命中最多,则可以利用预取指令来避免缓存未命中。

有一种事件不需要硬件支持也可以进行度量的就是缺页(page fault)。操作系统负责解决缺页,此时它也会记录这些。有两种类型的缺页:

次要缺页
这种缺页是那种暂时没有使用的匿名页(anonymous pages),但不能是文件映射页面(file-backed pages),例如copy-on-write页面和那些页面的内容已经存在于内存的情况。
主要缺页
需要读取硬盘来获取已经交换出去的数据来解决缺页。

很明显,主要缺页会比次要缺页昂贵的多。不过后者也并不是廉价的。哪种情况下内核都需要生成新的表项,需要一个新的页面,页面要么是干干净净的要么是已经有了合适的数据,同时页表树也要被更新。最后一步需要同步其它在操作页表树的进程,这可能引入新的延迟。

最简单的获取缺页信息计数的方法是使用time工具。注意使用工具,而不是shell的内嵌命令。图7.4显示了输出。{前导的反斜杠组织使用内嵌的命令}

*译注:这里指要是GNU Time工具。

$ \time ls /etc
[...]
0.00user 0.00system 0:00.02elapsed 17%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (1major+335minor)pagefaults 0swaps

图7.4: time工具的输出

有趣的部分是最后一行。time工具报告了1个主要缺页和335个次要缺页。具体的数字差异很大,尤其是立即重复运行会显示根本没有主要缺页。如果程序执行相同的动作,并且环境不再改变,总的缺页数也保持稳定。

一个对缺页特别敏感的阶段是程序的启动。每个需要的页都会产生一次缺页;可见的影响(尤其是对GUI应用)就是需要越多的页面,程序开工的时间就越长。7.5节中我们会使用工具来具体测量这个影响。

在内部,time工具使用rusage结构。wait4系统调用会在等待子进程终止时填充好rusage结构,这正是time工具所需要的。不过对于一个进程来说也可以获取自身资源使用情况(这就是rusage名称含义所在)或它已终止的子进程的资源使用情况。

#include <sys/resource.h>
int getrusage(__rusage_who_t who, struct rusage *usage)

who参数指定哪个进程的信息需要捕捉。当前定义了RUSAGE_SELF和RUSAGE_CHILDREN宏。子进程的资源使用率会在子进程终止时递增。这是一个累加的值,不是某个具体的子进程的使用率。有建议要允许捕捉指定线程的信息,所以在不久的未来我们应该可以会有RUSAGE_THREAD宏。

*译注:Linux 2.6.26已经加入了RUSAGE_THREAD宏,http://manpages.courier-mta.org/htmlman2/getrusage.2.html

rusage结构体被定义为包含各种测量结果,包括运行时间,IPC消息的发送数量,内存使用和缺页数量。缺页数由结构体中的ru_minflt和ru_majflt域来记录。

当程序员试图找到他的程序中哪里因为缺页而导致性能问题时,可以通过捕捉此信息并比较每次的返回值。

从外部来看,这些信息对于那些拥有足够权限的用户也是可见的。伪文件/proc/<PID>/stat,其中<PID>是我们所关心进程的进程ID,从第10到14列包含了缺页数。分别为进程和其孩子的累计次要和主要缺页。

 

Rev 1.修正了一些翻译错误,统一了下术语

Rev 2.增加一些注释