内存性能工具:Part 2 模拟CPU缓存
2010-07-01 15:05 Robbin 阅读(2398) 评论(0) 编辑 收藏 举报7.2 模拟CPU缓存
对缓存是如何工作进行的技术描述相对易于理解,但却很难真正观察到一个实际的程序是如何受到缓存的影响。程序员对内存地址的具体值不太感兴趣,因为它们不是固定的就是相对固定的。地址是早就确定好的,部分是由链接器决定的,部分是在运行时由动态链接器和内核确定的。生成的汇编代码被要求可以工作在任意可能的地址上,在源码中也没有任何关于绝对地址的任何提示信息的存在。因此很难感受到一个程序是如何利用内存的。{当靠近硬件编程时情况可能有所不同,不过这和常规的编程没有关系,而且即使这样也只是对内存映射设备的这类特殊地址有意义}
CPU层面的剖析工具,如Oprofile(在7.1节有介绍),可以帮助了解缓存的使用。结果数据对应实际的硬件,并且在不需要细粒度收集时可以相对快速的收集到。当需要更细粒度的数据时,oprofile则不再适用;线程会被频繁的中断。如果想要观察程序在不同处理器上的内存行为,就需要有真实的机器并让程序在上面运行。很多时候这(通常)是不可能的。一个例子就是图3.8中的数据。通过Oprofile收集这些数据需要有24台不同的机器,而很多机器根本就没有。
图中的数据实际是通过一个缓存模拟器收集到的。这个程序,cachegrind,使用了valgrind框架,它最早是为了发现程序中内存处理的相关问题而开发的。valgrind框架模拟了程序的执行,同时它还支持各种扩展集成到执行框架中去,例如cachegrind。cachegrind工具使用此来截获所有使用的内存地址,然后它以给定的缓存大小,缓存行大小和关联度去模拟L1i,L1d和L2缓存的操作。
要使用这个工具,程序需要通过valgrind来运行。
valgrind --tool=cachegrind command arg
以这种最简单的形式程序command以参数arg执行,执行在和所运行的处理器相同大小和关联度的3个模拟缓存之上。程序执行时会将一部分输出会打印到标准错误输出上;它包含对全部缓存的统计结果,见图7.5。
==19645== I refs: 152,653,497==19645== I1 misses: 25,833==19645== L2i misses: 2,475==19645== I1 miss rate: 0.01%==19645== L2i miss rate: 0.00%==19645====19645== D refs: 56,857,129 (35,838,721 rd + 21,018,408 wr)==19645== D1 misses: 14,187 ( 12,451 rd + 1,736 wr)==19645== L2d misses: 7,701 ( 6,325 rd + 1,376 wr)==19645== D1 miss rate: 0.0% ( 0.0% + 0.0% )==19645== L2d miss rate: 0.0% ( 0.0% + 0.0% )==19645====19645== L2 refs: 40,020 ( 38,284 rd + 1,736 wr)==19645== L2 misses: 10,176 ( 8,800 rd + 1,376 wr)==19645== L2 miss rate: 0.0% ( 0.0% + 0.0% )图7.5: Cachegrind摘要输出
全部的指令数和内存引用次数都被给出了,同时还有因此造成的L1i/L1d和L2缓存的未命中次数,失效比(cache miss rate)等。工具甚至还可以将对L2缓存的访问划分为是指令还是数据,数据缓存的访问可以划分为是读还是写。
当模拟的缓存的具体细节变化时会变得更加有趣且结果也可以进行比较。通过使用--I1,--D1和L2参数,cachegrind可以忽略处理器缓存布局而使用命令行指定的参数。例如:
valgrind --tool=cachegrind --L2=8388608,8,64 command arg
会模拟出一个有8路组相连的8MB L2缓存且有64字节的缓存行大小。注意--L2选项要放在要模拟执行的程序的名字之前。
这还不是cachegrind的全部功能。进程退出时cachegrind会产生一个名字为cachegrind.out.XXXXX的文件,其中XXXXX是进程的PID。这个文件包含了各个函数和文件对缓存使用的摘要和完整信息。这些数据可以使用cg_annotated程序查看。
进程终止时,程序会输出包含缓存使用的摘要信息,包含程序中每个函数使用的缓存行的详细摘要。生成每函数的数据需要cg_annotate能够将地址匹配到函数。这就需要包含调试信息以获得最近效果。如果没有,那么ELF符号表可能有点用,但由于内部符号不会包含在动态符合表中,结果也就不完整了。图7.6显示了图7.5中程序执行后的部分结果。
--------------------------------------------------------------------------------
Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw file:function
--------------------------------------------------------------------------------
53,684,905 9 8 9,589,531 13 3 5,820,373 14 0 ???:_IO_file_xsputn@@GLIBC_2.2.5
36,925,729 6,267 114 11,205,241 74 18 7,123,370 22 0 ???:vfprintf
11,845,373 22 2 3,126,914 46 22 1,563,457 0 0 ???:__find_specmb
6,004,482 40 10 697,872 1,744 484 0 0 0 ???:strlen
5,008,448 3 2 1,450,093 370 118 0 0 0 ???:strcmp
3,316,589 24 4 757,523 0 0 540,952 0 0 ???:_IO_padn
2,825,541 3 3 290,222 5 1 216,403 0 0 ???:_itoa_word
2,628,466 9 6 730,059 0 0 358,215 0 0 ???:_IO_file_overflow@@GLIBC_2.2.5
2,504,211 4 4 762,151 2 0 598,833 3 0 ???:_IO_do_write@@GLIBC_2.2.5
2,296,142 32 7 616,490 88 0 321,848 0 0 dwarf_child.c:__libdw_find_attr
2,184,153 2,876 20 503,805 67 0 435,562 0 0 ???:__dcigettext
2,014,243 3 3 435,512 1 1 272,195 4 0 ???:_IO_file_write@@GLIBC_2.2.5
1,988,697 2,804 4 656,112 380 0 47,847 1 1 ???:getenv
1,973,463 27 6 597,768 15 0 420,805 0 0 dwarf_getattrs.c:dwarf_getattrs
图7.6: cg_annotate输出
Ir,Dr和Dw列显示了全部缓存使用值,而不是缓存未命中值,后者在随后的两列显示。这些数据可以用于找出那些代码产生了最多的缓存未命中。首先,可以关注与L2缓存未命中,然后转而去优化L1i/L1d缓存未命中。
cg_annotate可以提供更详细的数据。如果指定了某个源文件,它可以标出该源文件上每一行的缓存命中和未命中情况。这些信息可以是程序员直达缓存未命中有问题的具体代码行。程序接口有些原始,在写此文档之时,cachegrind数据文件和源文件必须在同一个目录下。
在这里需要特别注意的是:cachegrind只是一个模拟器,它完全没有使用实际处理器的测量数据。处理器真正的缓存实现可能完全不同于此。cachegrind模拟了最近最少使用(LRU)替换策略,这对有很高关联度的缓存可能是过于昂贵了。除此以外,模拟没有包含上下文切换和系统调用的开销,它们都会大范围破坏L2数据并且是L1i和L1d进行刷新。这导致缓存不命中的总数会低于实际测量值。然而,cachegrind对于程序员了解内存使用情况和内存问题依然是非常有用的工具。
*译注:cachegrind也不会模拟预取,所以缓存未命中也可能高于实际情况。