测试准备:函数首地址

    在测试进行之前,必须解决一些执行代码的问题,让执行代码对程序的影响减到最小,才能使测试数据真实地反映数据通道的性能。

    第一个要解决的问题是函数的首地址对程序性能的影响。在有指令预取的系统中,函数的首地址会对程序的性能有影响,特别是对小循环会有很大的影响。如下图所示:

预取单位

预取单位

       

一个小循环跨越2个预取单位,则CPU必须用2次预取才能执行一次循环。其中绿色部分虽然都被取到CPU内部,但是不会有任何用处,导致资源浪费。Intel和AMD的文档称,Pentium Pro系列和Athlon/Duron系列的预取单位是16字节,Intel的软件优化建议还中建议把函数和循环的首地址16字节对齐。对于这些信息,我们不能就这样相信,必须通过程序测试出来。毕竟本网站的目的就是测试各种各样的参数。

    要进行这样的测试,就必须要能够把一个函数放到内存中的指定位置。这需要几个前提:(1)代码可以象数据一样访问(读、写、复制);(2)数据可以被当作代码执行;(3)可以知道函数的首地址和大小。前提(1)比较容易解决。在Win32环境下,程序采用的是平坦地址空间模型,代码和数据在同一个空间,代码可以被当作只读的数据。前提(3)稍麻烦。函数的的首地址很容易解决,C语言和汇编语言的函数名代表的是指向函数首地址的指针。函数的大小在各种程序语言中都是不提供的,必须用一些特殊方法获得。不过这也不是难事。在汇编语言中,函数返回是ret指令完成的,但是汇编语言并没有对ret指令进行特殊处理。所以,在ret指令后仍然可以写汇编代码。如果我们在ret指令后加一个标号,则该标号就是函数结束的地址。用这个地址减去函数的首地址就得到了函数的大小。前提(2)的实现必须有操作系统的帮助。在Win32API中,正好有这样的功能。其函数VirtualAlloc可以分配一块内存,并指定该内存为“可执行”的,还有函数VirtualProtect函数可以把某段内存的类型修改为“可执行”的。结合使用这2个函数,就可以实现我们想要的功能。具体实现细节就不讨论了,见测试程序源码和Win32API的文档(测试程序源码将在近期发布)。

    下面就是测试程序的第一个测试结果(PIII-M650@500):

PreBench_Tb01.gif (21819 bytes)

其中每列代表一个函数,共用了6个不同的函数进行了测试。每行代表不同的函数起始偏移。测试值是数据传输的速度,单位为字节/周期(Byte/cycle,B/c),越大越好。由于Intel和AMD都宣称预取大小为16,我们测试0~31这个范围。首先看第一列数据。可以明显看出0~15的数据和16~31的数据完全一致。下面是依据这些数据画的曲线图:

PreBench01.gif (4916 bytes)

二条曲线完全重合,说明函数首地址偏移在16~31时的情况和0~15完全时完全相同。其它几组数据也有相同的规律。这说明PIII的指令预取单位确实是16字节。所以以后将只考虑函数起始地址偏移在0~15变化的情况。

    下一个要确定的问题是函数首地址取何值时有最佳性能。这里测试的6个函数有一些共同特点:(1)都有一个循环,该循环要执行非常多次;(2)测试时记录循环体内执行的时钟周期数,循环体外的代码不进入时间统计;(3)函数入口到循环入口的代码完全相同,或者说循环入口相对于函数入口的偏移是一个常数。根据这些特点,可以得到如下结论:(1)改变函数入口地址可以导致循环入口地址的改变(好象是废话!?);(2)循环入口地址相对于最近的16字节地址边界的值是函数入口地址的函数而与函数本身无关:

    Offset_Loop_Entry=f(Offset_Func_Entry)

话有些拗口,不过意思时严谨的。再参考Intel的优化建议,应该将循环入口对齐到16字节边界,则可知如果某个函数在入口地址为x时达到性能最佳值,其它函数也应该在此达到性能最佳。是不是这种情况,数据分析以后才知道。

    将上表中Read/2、Write/2和Copy/2这3个函数偏移为0~15的数据画成曲线图如下:

PreBench02.gif (4385 bytes)

曲线形状差异很大是意料中的。毕竟函数各有特点,不能要求其“共进共退”。但根据Intel的优化建议,3曲线应该在同一偏移处达到最高点。实际情况却不是这样的。Copy/2的最高点是其余2个函数性能较差的地方!?还有一个有趣的现象是3曲线在偏移为12处同时达到最低点。我们再检查汇编程序的输出,可以发现函数在循环入口前的字节数为0x44,加上12正好是0x50,是Intel建议的入口地址!!??这不禁让我对自己产生了怀疑:是否计算错误,应该数据越小越好??此处暂将该问题搁置,后面再来论证。

    还有一个问题,在上面说过,循环的入口地址对“小”循环的性能有影响。当循环变大以后,其影响是否还在?在上面的测试中,6个函数共可分成3组,Read/2和Read/3是一组,其实现的功能完全相同,只是其大小不同:Read/3的循环大小为Read/2的2倍(如何做到这一点将在以后的文章中阐述)。其余4个函数也有类似关系。我们把Read/2和Read/3的数据画成如下曲线图:

PreBench03.gif (3829 bytes)

图中曲线的关系可能显得不太明显。不过应该可以看出来:大的函数(Read/3)的曲线较平缓,而小函数(Read/2)的曲线波动较大。当然仅凭这个图就得出这样的结论是很武断的,我们还有更详细的测试数据反映这个现象。

    下一个测试专门反映不同函数“大小”对起始偏移的敏感程度。

PreBench_Tb02.gif (10242 bytes)

表中每一列代表某一个“功能”,每一行代表实现该功能的函数的大小(每下降一行,循环体的自己数增加一倍。实现细节在以后的文章中讨论)。测试过程是:(1)根据指定功能和指定大小生成函数;(2)把该函数定位到内存中,起始地址偏移取0~15;(3)运行函数,得到性能指标(共16个,分别对应不同的起始偏移);(4)将16个性能值中最大的(设为MAX)和最小的(设为MIN)取出,计算:

    (MAX-MIN)*100/MIN

这样就得到了表格中的一个数据。每个数据都是如此获得的。我们姑且把这个指标叫做“波动系数”。

    考察上面的计算公式,其含义是性能最好时比性能最差时“快”了多少,所以反映了一个函数对起始地址的敏感程度。该值越小,说明函数对起始地址越不敏感。查看上面表格的数据,可以发现,整体趋势是随着函数增大,对起始地址的敏感程度将迅速下降。绝大多数函数在大小增加到一定程度后基本上可以认为函数起始偏移对函数没有影响了。表格中最后一行的数据是函数的波动系数小于0.4时函数的大小。其含义是:当实现某功能的函数大小大于某特定值后,函数首地址对函数性能的影响将小于0.4%。对小于0.4%的差异,一般情况下都可以不予考虑。在后续测试中,将利用这个值节约计算量。

    在表中CP/04与众不同。无论函数体积增加到多大,其性能均对函数首地址敏感,波动在11%左右。这个波动值是非常大了,在测试中必须予以考虑。至于产生原因尚不得而知。

    另外,Intel宣称P4不再受函数首地址偏移的影响,因为其采用的Trace Cache工作原理与需要指令预取的普通I-Cache不同。具体情况还未测试过,暂时不能下结论。

    关于函数的首地址的讨论就到此为止。有更好的想法请与我联系!


Leading Cloud Surveillance, Recording and Storage service; IP camera live viewing

Leading Enterprise Cloud IT Service; cloud file server, FTP Hosting, Online Storage, Backup and Sharing

Powered by FirstCloudIT.com, a division of DriveHQ, the leading Cloud IT and Cloud Surveillance Service provider since 2003.