测试准备:测试函数

    在进行测试之前,必须介绍一下核心测试函数,因为在源代码中你将不能看到完整的测试函数,只能看到连接函数将要使用的一个个代码片断。

    首先要说明一下函数需要的参数。我们希望读/写/复制操作都可以一致地完成,所以首先要有2个指针参数,我们叫做pSrc和pDst。读和写操作针对pSrc所指区域操作,而复制操作将同时使用2个区域。接下来是内存的字节大小(MemSize)和块(跳跃)的字节大小(BlockSize),其含义都是很直接的。另外我们还需要2个参数:ItemSize和LoopCount。LoopCount是循环次数,没有什么疑问。ItemSize是指每次操作的单元大小。由于我们的测试涉及ALU、MMX和SSE等3个部件,对应的操作其每次处理的数据量是不同的分别为4、8和16字节。在这里需要这个参数来进行一些地址运算。综合起来,函数的C语言原型如下:

unsigned __int64 TBenchFunc(void *pSrc,void *pDst,int MemSize,int BlockSize,int ItemSize,int LoopCount);

返回值是循环体执行的周期数。

    下面是测试函数的流程图:

PreBench_Func_02.gif (4283 bytes)

    在测试中,核心语句序列总是向前递增地址。在达到内存块的末尾的时候,如以前讨论操作顺序时指出的,要绕回到内存块的开始部分。所以测试共有2重循环:内循环处理一遍内存(指地址增加到内存块末尾处,并不一定每个内存单元都访问到了),外循环保证整个核心语句序列执行足够多次数。


    在测试运行过程中,我们不希望把任何临时变量放到内存中,因为这会影响CACHE的运行。上面的函数共有6个参数,再加上一些临时中间变量,通用寄存器组将没有足够空间。为此我们将使用MMX寄存器来存储一些临时变量。这样在循环中除了核心操作以外,没有内存访问指令。当然坏处是不能在不支持MMX指令的机器上运行。这应该没有太大的问题。

    下面是测试函数的具体讨论。在循环入口以前的辅助代码清单如下:

PreBench_Func_01.gif (6238 bytes)

由于这部分代码不进入时间统计,没有对其进行优化。

开始4行是保存要使用的寄存器。EAX、ECX和EDX可以任意使用而不恢复其值,ESP是栈指针不能使用。其余4个寄存器都保存了。

接着是把pSrc加载到ESI中:

    mov  esi,  pSrc

各个变量在栈中的地址在函数之前由宏定义,所以这里只要引用变量名字就可以了。

然后是加载每次的地址修正量:

    mov  ebp,  AddrInc

AddrInc是一个常量,是以前讨论访问顺序时所说的BlockSize*n。但在编译时不可能知道其值,所以这里填入的只是一个虚值,在运行时将由连接函数改成实际值。该指令后面的标号AddrIncHelp就是为了定位这个需要修改的常量的位置而设置的。

接着是加载pDst到EDI:

    mov  edi,  pDst

后面的4条减法指令是以前讨论访问顺序时所说的“进入循环前的修正”,含义应该很直接。执行到这里,ESI和EDI中存储了循环开始时应该有的地址。但在循环中,我们要修改ESI和EDI的值,从而在某些时候要求把这2个寄存器的值恢复到现在的内容。所以需要把这2个寄存器的当前值保存到MMX寄存器中以备将来使用。由于MMX寄存器是64位的,只需要为这2个变量分配一个MMX寄存器就可以了。实际分配的寄存器是由前面的宏AddrR定义的,这里只要引用这个宏就可以了。但并没有指令可以把通用寄存器对的值拼成64位传递到MMX寄存器中,所以这里用下面的3条MMX指令把ESI和EDI存储到AddrR中:

    movd  AddrR,   esi        ;传递ESI

    movd  mm0,     edi        ;传递EDI

    punpckldq  AddrR,  mm0   ;拼成一个64位单元

在AddrR中,低32位是ESI,高32位是EDI。在这中间有下面的一条指令:

    mov  ebx,  AdjustFreq

这里AdjustFreq是一个常量,是指要进行多少次核心循环才需要恢复一次ESI和EDI。这就是前面流程图中的内循环的次数。这个常量也是在连接的时候由程序计算出来填到这里的,所以后面有一个AdjustFreqHelp标号帮助定位地址。在每次外循环中,这个值也是要被恢复的,所以后面有把这个变量保存到MMX寄存器的指令:

    movd  AdjustFreqR,  ebx

另外,我们把ESI和EDI存储到MMX寄存器中,除了节约通用寄存器外,还希望利用MMX指令进行地址绕回处理。利用MMX的并行计算特点,2个地址(ESI和EDI)的绕回处理只需要一次运算就足够了,这也是我们把ESI和EDI合并到一个MMX寄存器的目的之一。不过,在进行地址绕回处理的时候,还需要用BlockSize和ItemSize两个变量。所以下面的指令序列把BlockSize和ItemSize存储到MMX寄存器中:

存储BlockSize 存储ItemSize
mov   eax,  BlockSize
movd  BlockSizeR,  eax
punpckldq  BlockSizeR,  BlockSizeR
mov   eax, ItemSize
movd  ItemSizeR,  eax
punpckldq  ItemSizeR,  ItemSizeR

最后一条指令的目的是使MMX寄存高32位和低32位内容相同,为后面的并行地址绕回处理作准备。

在这些代码中间有一条指令:

    pxor    InBlockR,   InBlockR

其中InBlockR是一个MMX寄存器。该寄存器存储的是当前处理循环中块内的偏移。请参考关于操作顺序的文章:当第一遍处理(外循环的第一次)的时候,访问所有块的第一个单元,此时InBlockR为0......第k遍时访问所有块的第k个单元,InBlockR的值为k×ItemSize;当k×ItemSize>=BlockSize时,要将InBlockR重新清零。利用InBlockR这个临时变量,可以很方便的进行地址绕回处理。

下面的代码是设置外循环的计数:

mov eax, LoopCount
xor edx, edx
div ebx
mov ecx, eax

在测试中我们传进来的参数LoopCount是指核心语句序列要执行的次数,所以这里要先除以内循环的次数才是外循环的循环计数。

下面的代码是为顺序操作特殊准备的:

        mov eax, BlockSize
        cmp eax, ItemSize
        jne short NoAddr2
        movd AddrR2, edi ;BlockSize==ItemSize:Sequencial
NoAddr2:

顺序操作不需要复杂的地址绕回处理,所以将对其进行特殊处理,以减少循环附加开销。判断是否顺序顺序操作很简单:如果BlockSize==ItemSize就是顺序操作,否则不是。特殊处理也很简单,就是另外分配一个MMX寄存器(AddrR2)来存储EDI。

下面就是记录进入循环的时间了:

rdtsc
movd TscR, eax
mov eax, pSrc
movd mm0, edx
punpckldq TscR, mm0

rdtsc指令读出当前的时钟计数到EAX:EDX中。不过我们需要把EAX和EDX释放出来作它用,所以还要把它们的值传递到MMX寄存器(TscR)中保存。在这中间有一条指令:

    mov  eax,  pSrc

是为两个特殊测试(有数据相关的读和复制)准备的。在测试程序中将会有几个特殊测试,我们将在以后用到的时候再讨论。

至此固定的循环初始化就算完成了。不过根据不同的测试,在进入循环前还有少量的不同初始化指令。

对有数据相关的复制操作,需要在后面紧跟一条指令:

    mov   edx,   MemSize

对SIB地址方式(有数据相关的测试不会采用这种地址方式),需要在后面紧跟另一条指令:

    xor   edx,   edx

对其它测试则不需要额外的初始化指令。这些都由程序判断然后根据需要连接到一起。

    核心语句序列已经在前面讨论过,这里就不再讨论了。下面讨论进行地址绕回判断和地址绕回处理。

    由于我们已经把内循环的计数放到EBX中,所以地址绕回判断也是很直接的:

            sub   ebx,    1  ;Intel建议用SUB而不用DEC

            jnz    loop_start

前面已经讨论过,那条sub指令将插入核心语句序列最后一块的开头,所以在循环末尾只需要连接条件转移就可以了。要注意的是当所操作的内存块比较小而展开次数又比较大的时候,内循环可能只有1次。此时不需要进行内地址绕回判断。连接函数将判断这种情况,根据需要决定是否要连接这个条件转移语句和前面的减指令。在要连接绕回判断的情况,由于循环展开的次数不同,条件转移的目的偏移是不同的。连接程序将根据实际情况修改这里的偏移。

    地址绕回处理分多种情况。首先看特殊测试的情况。某些特殊测试(如:有数据相关的操作、寄存器内的操作等)不需要地址绕回判断和处理。这种情况相对简单,连接函数将不连接相关的部分。此时连接程序还要把AdjustFreq置为1,保证计算出的外循环次数正确。

    接下来是顺序操作的情况。顺序操作的地址绕回也很简单,只要把ESI和EDI的值恢复了就可以。当然还有恢复内循环计数EBX的指令:

movd esi, AddrR
movd edi, AddrR2
movd ebx, AdjustFreqR

其中恢复EDI的指令仅有复制操作需要,连接函数也会根据需要选择。

    最后是普通的块跳跃操作的地址绕回处理。下面是代码清单:

paddd      InBlockR, ItemSizeR
movq        mm0,     BlockSizeR
pcmpgtd  mm0,      InBlockR
pand        InBlockR, mm0

movq        mm0,      AddrR
paddd      mm0,      InBlockR

movd        esi,      mm0

punpckhdq   mm0,      mm0
movd        edi,      mm0

movd        ebx,       AdjustFreqR

第一阶段的4条指令计算出下一个块内偏移,注意后3条指令的目的是让块内偏移大于或等于BlockSize后回到0。接下来的2条指令是把块内偏移和原来的地址相加,就得到了需要恢复的地址(mm0中)。后面的2个阶段分别是恢复ESI和EDI。当然恢复EDI的2条指令只在复制操作中才连接进去。最后的指令是恢复内层循环计数的。

    下面是循环尾部的处理了。其清单如下:

        sub ecx, 1
        jnz near ptr _BenchFunc
LoopAdjustHelp:
        rdtsc
        movd esi, TscR
        punpckhdq TscR, TscR
        movd edi, TscR

        emms

        sub eax, esi
        sbb edx, edi

        pop ebp
        pop edi
        pop esi
        pop ebx

        ret

前2条指令是进行外循环用的,意义很明确。其中的跳转指令的目的地址同样不能确定,必须在连接的时候由程序调整,所以后面有一个帮助获得其偏移地址的标号LoopAdjustHelp。紧接着的rdtsc是读退出循环时的计时值。后面的3条指令是把以前存储到MMX寄存器TscR中的进入循环的计时值读回到ESI和EDI中。再后面的代码都很简单,就不说了。

    测试函数的代码就这么多。应该说还是比较简单的。还可以看出,经过充分展开的测试函数其循环附加开销是很小的,基本上不会影响测试结果。在后面的测试中我们也将讨论这个问题。所以后面的一些理论分析可能不会考虑循环附加开销的问题。


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.