由于我自己使用的是P4和汇编,因此就P4和汇编这两个角度对我和pora的程序进行一些分析。
我将pora的程序通过VC.NET的汇编输出(编译器命令行 /O2 /Ob1 /Oy /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /GF /FD /EHsc /ML /GS /Gy /FAcs /Fa"Release/" /Fo"Release/" /Fd"Release/vc70.pdb" /W3 /nologo /c /Wp64 /Zi /TP),然后进行下面的比较:
将算法分为三个部分:主循环以及循环内的主要编码部分(S1),主循环内的行分割处理部分(S2),以及最后四个字节的处理部分。由于最后一部分对性能的影响极小,因此仅讨论前两部分。
不考虑算法,从汇编的角度上看,有以下数据:
指令数 指令长度 平均指令长度 循环数
Inno S1: 21 67 3.2 19
Inno S2: 5 11 2.2 1
pora S1: 22 83 3.8 19
pora S2: 3 13 4.3 1
注:
1、上面指令长度和平均指令长度的单位都是字节
2、循环数是指相对而言的循环次数比例。
3、关于pora的S2部分见下文的说明
下面对所用到的指令进行一下统计:
MOV ADD SHL CMP Jcc INC BSWAP
Inno S1: 6 6 4 2 2 1
Inno S2: 1 1 1 2
pora S1: 15 2 3 1 1
pora S2: 2 1
注:
ADD指令包括:ADD,AND,XOR
SHL指令包括:SHL,SHR
Jcc指令包括:JMP以及JGE、JLE等
INC指令包括:INC/DEC
下面是各指令的周期时间(根据Intel的Optimization Reference Manual):
潜伏期 指令时间 合计时间
MOV 0.5 0.5 1
ADD/AND/XOR 0.5 0.5 1
SHL/SHR 4 or 1 1 5
CMP 0.5 0.5 1
Jcc/JMP - 0.5 0.5
INC/DEC 1 0.5 1.5
BSWAP 7 1 8
注:
1、上表中单位均为指令周期
2、由于P4以乱序方式执行指令,对于SHL/SHR指令,其指令潜伏期可能最少为1
可以认为,一个指令的执行时间是潜伏期和指令执行时间之和,因此由上面两个表,可以得出“纯”编码循环在P4上的执行时间比例:
合计指令周期 总循环花费时间周期
Inno S1: 43 817
Inno S2: 5.5 5.5
pora S1: 33.5 636.5
pora S2: 3 3
这样得出理论时间为(每19个循环,单位是指令周期):
Inno: 822.5
pora: 639.5
比值为:1.29
而在我自己计算机上实测的结果为(单位为毫秒):
Inno: 335
pora: 305
比值为:1.10
从这点看,结果还是基本符合的。然而对于abp以及其他朋友测出来我的比pora的程序快100%以上,似乎有些问题。
对于这个分析结果看,有几点很有意思:
1、pora的代码中,MOV指令使用率为68%,而MOV指令是P4中最快的指令之一,因此使得速度有很大的提高;
2、移位指令花费的时间远比MOV、ADD等指令多,而我使用了BSWAP指令则更是最大的失败(但是就这个算法来说并没有别的指令可以代替这个功能);其实我原先用C++实现的这段算法中采用了更多的移位指令和逻辑指令,使得这个程序花费的时间是我现在实现的5倍以上!
3、pora将我们通常用的for(i=0; i+3<size; i=+3)用指针的比较(real_input==real_input_end)来代替,少了好几条指令;
3、前面诸位讨论的读4个CHAR和读1个DWORD的事情,就从指令周期来看显然是读取1个DWORD更加快,因为前者需要四条MOV指令;
4、VC对pora的程序进行了一个很有意思的优化,将循环展开:
代码:
优化前:
for(;;)
{
real_output[0]=BASE64_CODE_SHIFT4[real_input[0]];
real_output[1]=BASE64_CODE[real_input[0]*16+real_input[1]/16];
real_output[2]=BASE64_CODE[real_input[1]*4+real_input[2]/64];
real_output[3]=BASE64_CODE[real_input[2]];
real_input+=INPUT_CELL;
real_output+=OUTPUT_CELL;
if(real_input==real_input_end)
break;
--ticks;
if(!ticks)
{
ticks=LINE_COUNT/OUTPUT_CELL;
*(unsigned short*)real_output='\r\n';
real_output+=sizeof(unsigned short);
}
}
成为这样的代码:
代码:
优化后:
real_output[0]=BASE64_CODE_SHIFT4[real_input[0]];
real_output[1]=BASE64_CODE[real_input[0]*16+real_input[1]/16];
real_output[2]=BASE64_CODE[real_input[1]*4+real_input[2]/64];
real_output[3]=BASE64_CODE[real_input[2]];
real_input+=INPUT_CELL;
real_output+=OUTPUT_CELL;
if(real_input==real_input_end)
break;
l1:
--ticks;
if(!ticks)
{
ticks=LINE_COUNT/OUTPUT_CELL;
*(unsigned short*)real_output='\r\n';
real_output+=sizeof(unsigned short);
}
real_output[0]=BASE64_CODE_SHIFT4[real_input[0]];
real_output[1]=BASE64_CODE[real_input[0]*16+real_input[1]/16];
real_output[2]=BASE64_CODE[real_input[1]*4+real_input[2]/64];
real_output[3]=BASE64_CODE[real_input[2]];
real_input+=INPUT_CELL;
real_output+=OUTPUT_CELL;
if(real_input==real_input_end)
break;
goto l1;
如何评价这个优化呢?我看不出这个优化有什么作用,最多省略一次比较而已,但是要增加一倍的代码,似乎有些得不偿失。
上次pora和我说,或许这个程序还能更加快一点,不过我现在认为已经不会更快了,因为VC的优化实际上已经很彻底了。
由于我手边没有AMD CPU的资料,因此暂时不能分析在AMD机器上的情况。