1983年,一位程序员TomDuff(曾在贝尔实验室和星球大战母公司卢卡斯影业就职过),在参与图形渲染的软件中尝试优化图像数据传输到帧缓冲区的过程中遇到了性能瓶颈,写下了一段奇怪的代码:
void send( int * to, int * from, int count)
{int n = (count + 7 ) / 8 ;switch (count % 8 ) {case 0 : do { * to ++ = * from ++ ;case 7 : * to ++ = * from ++ ;case 6 : * to ++ = * from ++ ;case 5 : * to ++ = * from ++ ;case 4 : * to ++ = * from ++ ;case 3 : * to ++ = * from ++ ;case 2 : * to ++ = * from ++ ;case 1 : * to ++ = * from ++ ;} while ( -- n > 0 );}
}
乍一眼看上去,switch和while嵌套使用,这段代码好像有Bug
但是它不仅没有Bug并且极其巧妙
下面将一步一步讲讲它是怎么来的:
问题起源
首先,在C语言中,如果要把一段字符复制到另一个区域,可以这么写:
void send(register short* to, register short* from, register int count)
{do { *to++ = *from++;} while (--count > 0);
}
//使用 register 关键字提示编译器尽可能地将这些变量存储在寄存器中以提高访问速度,不过,现代编译器通常会忽略这个提示,因为它们有自己的优化机制来决定哪些变量应该放入寄存器//现在推荐使用memcpy函数,编译器做了大量优化,当然memcpy本身也可以做优化,我以后有机会在后面文章补充相关的知识
send
函数的作用是将 count
个短整型数据项从 from
指向的数组复制到 to
指向的数组,也就是这是一个非常简单的内存复制操作,但是如果有10000个count的话,将会复制10000遍,显然效率低下
先进行展开优化
我们可以通过展开降低其循环次数
void send2(short* to, short* from, int count)
{int n = count / 8;do {*to++ = *from++;*to++ = *from++;*to++ = *from++;*to++ = *from++;*to++ = *from++;*to++ = *from++;*to++ = *from++;*to++ = *from++;} while (--n > 0);
}//现代编译器在编译阶段也会执行这个展开优化,我这里为了讲清楚代码,就暂时当编译器没有优化吧
这里降低了判断次数,原来10000次的循环,现在只需要执行1250次,但是这样会有一个限制,就是count必须为8的倍数,也就是 count%8 = 0
达夫的终极优化
void send3(short* to, short* from, int count)
{int n = (count + 7) / 8;switch (count % 8) {case 0: do { *to++ = *from++;case 7: *to++ = *from++;case 6: *to++ = *from++;case 5: *to++ = *from++;case 4: *to++ = *from++;case 3: *to++ = *from++;case 2: *to++ = *from++;case 1: *to++ = *from++;//这里没有break,会进入while循环的开头} while (--n > 0);}
}
现在count不必局限于8的限制了,下面讲讲它的过程,
①当count为8,16时,执行do—while中case0到case1的所有语句,直到n被减少到0,跳出循环
②当count为13时
第一趟:
n = 2,count%8 = 5,这时进入 case5
依次执行case5,case4,case3,case2,case1
这时没有break中断循环,又进入了while的循环
第二趟:
然后判断--n后,发现n = 1,执行 case0-case1后的代码
依次执行case0,case7,case6,case5,case4,case3,case2,case1
然后判断--n后,发现n = 0,退出循环
总共执行了13次case,就退出了循环,完成了复制的目的
全流程图: