小常数乘法:乘以10

    虽然现代计算机越来越快,但是做乘法的速度总是比做加法要慢很多(仅只通用CPU)。而变量乘以小常数的运算是很常用的乘法运算,特别是很多地址运算就是小常数乘法(这也是在这里讨论这个问题的原因)。

    一般来说,在很多情况下通过移位和加法运算可以比乘法指令的效率高,特别是象P4这种乘法指令延迟和产出都很低的CPU上。下面我们就具体看一下乘10这个问题。

    首先使用乘法指令肯定是一种解决途径。在x86指令系统,乘法指令可以直接跟一个常数,或者把2个寄存器相乘,其格式如下:

乘以常数 乘以寄存器
imul eax, 10 imul eax, edx

在这里我们都是用的得到32位结果的指令。得到64位结果的指令会慢一些。在测试中,分别把前面的2种测试叫做IMULC(Multiply by constant)和IMULR(Multiply by register)。

    接下来是用加法和移位的组合来实现。对乘10来说,通常有2种做法:(1)V×2+V×8;(2)(V×2)×5。第(1)种做法主要依赖移位运算:对乘以2^x形式的数的乘法,可以用移位实现。另外,x86指令系统有lea指令,可以快速地执行一次移位和2次加法(仅对有地址生成单元的CPU)。所以,我们可以用lea指令实现方案(1)如下:

lea     ebp,     [eax+eax]
lea     eax,     [ebp+eax*8]

在测试中我们把这个方案叫做LEA2R2,表示使用了2条lea指令和2个寄存器。这里使用了一个额外的寄存器ebp存储中间结果,可能是我们不愿意看到的。所以我们有第(2)种做法。方案(2)最直接的实现是:

add     eax,     eax
lea     eax,     [eax+eax*4]

得益于lea指令,我们可以在一条指令内实现乘以5。在测试中我们把这种方案叫做LEA1R1。另外,乘2操作还有左移1位的等价形式,所以有人认为下面的格式应该是一样好的:

shl     eax,     1
lea     eax,     [eax+eax*4]

至于是否是一样的,我们测试以后再说。在测试中我们把这个方案叫做SHL1R1。

    可以看出第(2)种形式的2个方案都不需要额外寄存器,看起来LEA2R2是不好的形式。但这只是针对PIII这种具有快速地址运算单元的CPU而言的。我们知道,在P4上,地址单元被取消了,地址运算将分解成很多的ALU运算。而P4有一个问题,就是移位有4个周期的延迟(!!??)。在上面的3个实现方案中,地址部分都有比例运算,这将被翻译成移位运算,将会有4个周期的延迟!!所以,根据数据相关的分析,LEA2R2在P4上的理论延迟是4.5周期,而LEA1R1将有5周期的延迟。SHL1R1由于使用了移位指令,如果没有对左移1位的特殊优化,将有8.5周期的延迟(!!??)。在这里看来,SHL1R1在P4上肯定比LEA1R1差了。而在PIII上的表现,我们测试后再比较。

    另外,我们还知道,Intel称P4有2个以主频2倍速度工作的ALU可以完成加、减法运算。如果P4的ALU是真正的2倍速(即:在后半个周期可以执行),则仅用1个ALU,4个周期在P4上就可以完成8次有数据相关加法,可以实现乘以256的操作了。所以,我们认为可以只用加法,而不用移位来实现延迟少于4个周期的乘以10。下面是据此实现的代码:

lea     ebp,     [eax+eax]
add     eax,     eax
add     ebp,     ebp
add     ebp,     ebp
add     eax,     ebp

根据分析,上面的代码在P4上理论延迟为2(真正的2倍速ALU)或4(伪2倍速ALU)。在这2种情况下,上面3个方案在P4上的理论延迟分别为4.5、5、8.5(真正的2倍速ALU,但用这里给出的测试方法,可能测出的值是5、5、9)和5、5、9(伪2倍速ALU)。可见,理论上这种实现方案是较好的。所以我们在测试中也测试了这种方案。我们把这种方案叫做Add4R2(4个加法指令,2个寄存器)。

    测试中,我们同时测试所有方案的延迟和产出。对延迟的测试,采取的是不停地对一个寄存器进行乘以10的操作,这样利用数据相关使流水线等待上一条指令的完成。对产出的测试,我们并行地对5个寄存器进行乘以10的运算,从而让流水线有足够的无关指令可以调度(对延迟大于5个周期的方案,可能仍然无法得到精确延迟。此时可能需要打破数据相关链的指令,如xor eax, eax)。下面是我们得到的测试结果:

Mul10_Tb01.gif (7644 bytes)

最左面一列是循环展开因子。为了便于比较和分析,还同时测试了左移1位和左移3位的测试,列SHL1对应左移一位,SHL3对应左移3位。最后一行是实现方案的指令字节数。看延迟的数据,LEA2R2、LEA1R1和SHL1R1的延迟都是2,应该认为没有什么区别。IMULC和IMULR的延迟都是4,说明PIII的乘法单元确实很强劲。Add4R2的延迟比4略高,性能最差,是在预料中的。我们再看产出的数据。IMULC、IMULR和LEA1R1都可以认为达到了每周期一个运算的速度,而LEA2R2和SHL1R1要2个周期才能产出一个运算结果,Add4R2性能最差是预料中的。

    从这个结果我们可以看出,在PIII上,LEA1R1方案应该是最佳的选择。虽然Intel建议采用IMULC的格式,但是其延迟太大,而这些小常数乘法的结果通常在后面马上就要使用,无法找到足够的无关指令来填满这个延迟(4个周期可以执行8条运算指令加上4条内存访问指令)。

    开始被认为和LEA1R1等价的形式SHL1R1明显比如LEA1R1,说明并不能根据简单的指令等价性进行判断。另外,这里的数据反映出2个问题:(1)PIII仅有一个移位部件,而且这个移位部件在地址生成单元(AGU)所在的调度端口(PORT0);(2)PIII的移位部件延迟是1个周期,性能还是不错的。

    LEA2R2的产出不如LEA1R1是预料中的,因为PIII仅有一个地址生成单元。LEA1R1的加法指令可以在PORT1上并行调度,可以达到更高的并行程度。不过在Athlon上面,由于Athlon有3个地址生成单元,这2种方案应该有相同的性能。

    在Intel的建议中,举的例子是乘以34。Intel给的代码是:

mov    ecx,    eax
shl    eax,    4
add    eax,    ecx
shl    eax,    1

    这个代码序列的理论延迟是3周期(PIII),和imul的4周期比起来,确实没有什么优势。但是,这个代码是很白痴的。如果可以使用一个临时寄存器,则优化的代码应该是:

mov    ecx,    eax
shl    eax,    5
lea    eax,    [eax+ecx*2]

    这个代码的理论延迟是2周期(PIII),应该说比imul的选择要好。所以,lea指令对x86指令系统是非常重要的,而Intel决意在P4中放弃地址生成单元和快速移位部件,简直是难以理解。

我没有其它的平台,就不比较其它CPU上的性能了。


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.