循环,不夸张地说,是每个程序员每天都要遇到的。作为结构化编程的三个主要结构之一,循环出现在每一个软件项目的几乎每一个源程序里。当一件事被放入循环的时候,就表明这件事可能会被做很多次,这也使得循环往往是优化的最佳位置。
很少有人想过,循环本身也是一个很大的开销。尤其是当循环体本身的运行速度很快的时候。譬如说,下面这个函数,可能每个人都曾经写过:
int search( const int* pi, int iSize, int iValue )
{
for ( int i = 0; i < iSize; ++i )
if ( pi[ i ] == iValue )
return i;
return -1;
}
这个函数用来在一个整型数组里查找第一个与给定值相等的整数的索引值。如果我们把这个函数用更直接(对编译器来说)的方法写出来,可能是
int expanded_search( const int* pi, int iSize, int iValue )
{
int i = 0;
goto judge;
loop:
if ( pi[ i ] == iValue )
return i;
judge:
++i;
if ( i < iSize )
goto loop;
return -1;
}
请注意,循环代码本身和循环体几乎消耗了相同的时间。也就是说,如果我们能够避免这种循环方式(循环本身是不可能避免的)的话,我们可以获得几乎100%的速度提升。Ronald E. Knuth在他的巨著The Art of Computer Programming中提到了这样一种方法:
int guard_search( int* pi, int iSize, int iValue )
{
int* p = pi;
p[ iSize ] = iValue;
while ( *p != iValue )
++p;
return p == pi + iSize ? -1 : p - pi;
}
与其它方法相比,唯一的要求是:供搜索的数组最后必须有一个元素是空的。这在实际程序里非常容易实现,尤其是在需要以此来换取执行速度的情况下。从测试结果(如下)可以看到,优化的效果相当显著。
search :380
expanded_search :371
guard_search :190
有的时候,你没有办法实现设定一个守护(guard)变量,或者你无法提供那个最后的空元素。例如下面的这个函数:
void assign( int* piDest, const int* piSrc, int iSize )
{
for ( int i = 0; i < iSize; ++i )
*piDest++ = *piSrc++ + 1;
}
在这种情况下,Knuth算法就不能使用了。这时,我们可以使用Duff's Device----由Duff提出了一个算法。修改后的函数如下:
void duff_assign( int* piDest, const int* piSrc, int iSize )
{
int i;
switch ( iSize % 4 )
{
case 0:
for ( i = 0; i < iSize / 4; ++i )
{
*piDest++ = *piSrc+++1;
case 1:
*piDest++ = *piSrc+++1;
case 2:
*piDest++ = *piSrc+++1;
case 3:
*piDest++ = *piSrc+++1;
}
}
}
可以看到,这个函数非常"丑陋",第一眼看上去,甚至怀疑它是否能通过编译。事实上,这是对C/C++语法的一种"特殊"使用方式,其主要目的是,减少在循环里的跳转和判断,以在每次循环执行时循环体获得更多的比重。
测试结果如下:
不使用Duff's Device:10235
使用Duff's Device:9263
提高了大约10%,虽然不多,但是如果能通过这样简单的改动,就可以使核心代码执行效率提高的话,也是很值得的。