文号
t31746
阅读
10243
回复
10
收藏
9
【转】古怪的“达夫设备”--快速数组复制法
2011/02/02
IP:江西
原文:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

前几天在网上看见了一段代码,叫做“Duff's Device”,后经验证它曾出现在Bjarne的TC++PL里面:

void send(int *to, int *from, int count)
        //   Duff设施,有帮助的注释被有意删去了
...{
        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语句和一个do-while语句糅合在了一起。而在我看过的所有关于C和C++的书中,这样的代码都是毫无道理的。然而,无论是在VS2005还是在GCC4.1.2下,这段代码都能正确地通过编译。加上适当的main函数,它都可以正常运行。我百思不得其解。上网去查,也没查到好答案。

怎么办?先看看它的汇编代码吧,也许可以通过它的汇编代码看出它的意思。

gcc -S send.cpp

粗略地一看,汇编代码都已经上百行了,而且里面还有一个跳转表,十几个标号。一般情况下,几十行的汇编代码都已经不太好看懂了,要把这几百行汇编完全看懂,估计需要花很多时间。

既然直接来太麻烦,那就用简便一点的方法吧:

#include <iostream>
using namespace std;

int main()
...{
    int n = 0;
    switch (n) ...{
    case 0:  do ...{cout << "0" << endl;
    case 1:         cout << "1" << endl;
    case 2:         cout << "2" << endl;
    case 3:         cout << "3" << endl;
            } while (--n > 0);
    }
}

实验结果
n的值     程序输出
0          0
1
2
3
1           1
2
3
2             2
3
0
1
2
3
3             3
0
1
2
3
0
1
2
3

这下终于弄清楚了。原来,那段代码的主体还是do-while循环,但这个循环的入口点并不一定是在do那里,而是由这个switch语句根据n,把循环的入口定在了几个case标号那里。也就是说,程序的执行流程是:程序一开始顺序执行,当它执行到了switch的时候,就会根据n的值,直接跳转到 case n那里(从此,这个swicth语句就再也没有用了)。程序继续顺序执行,再当它执行到while那里时,就会判断循环条件。若为真,则while循环开始,程序跳转到do那里开始执行循环(这时候由于已经没有了switch,所以后面的标号就变成普通标号了,即在没有goto语句的情况下就可以忽略掉这些标号了);为假,则退出循环,即程序中止。

忙活了几个小时,终于明白这段代码是怎么回事了。回想一下,自己以前也曾写过类似C的语法但比C语法简单很多的解释器,用的是递归子程序法。而如果用递归下降法来分析这段代码,是肯定会有问题的。

至于它是怎么正确编译并运行的,这需要去研究一下C编译器,这个以后再说。现在,还是再来看看达夫设备吧。其实,这个send函数的签名就已经很具有提示性了:把from数组中的元素拷贝count个到to里面去。于是有人会说,这个工作简单,不就这样吗:

void my_send(int *to, int *from, int count)
...{
    for (int i = 0; i != count; ++i) ...{
        *to++ = *from++;
    }
}

这段代码的确很简洁,也是正确的,而且生成的机器码也比send函数短很多。但是却忽略了一个因素:执行效率。计算一下就可以知道,my_send函数里面的循环条件,即i和count的比较运算的次数,是达夫设备的8倍!在做整数赋值这种耗时很少的工作时,这种耗时相对较高的比较工作是会大大地影响函数整体的效率的。达夫设备则是一种非常巧妙的解决办法(当然,它利用到了编译器的一些实现上的工作),而且如果把8换成更大的数的话,效率就还可以提高!

它的思路是这样的:把原数组以8个int为单位分成若干个小组,复制的时候以小组为单位复制,即一次复制8个 int。也就是说,在my_send函数中以一次比较运算的代价换来1个int的复制,而在达夫设备中,却能以一次比较运算的代价换来8个int的复制。而switch语句则是用来处理分组时剩下的不到8个的int(这些剩余的不是数组最后的,而是数组最开始的),很巧妙。

总结:像达夫设备这样的代码,从语言的角度来看,我个人觉得不值得我们借鉴。因为这毕竟不是“正常”的代码,至少C/C++标准不会保证这样的代码一定不会出错。另外,这种代码估计有很多人根本都没见过,如果自己写的代码别人看不懂,这也会是一件很让人头疼的事。然而,从算法的角度来看,我觉得达夫设备是个很高效、很值得我们去学习的东西。把一次消耗相对比较高的操作“分摊“到了多次消耗相对比较低的操作上面,就像vector<T>中实现可变长度的数组的思想那样,节省了大量的机器资源,也大大提高了程序的效率。这是值得我们学习的。


(PS:编译器自带的memcpy是优化过的,比自己写的要快很多~可能就是用这个代码)
15年11个月前IP:未同步
278447
1楼
这个代码确实写得挺精妙的,而且很符合标准啊,do-while本来就是if-goto啊,不可能会编译错误……

作为一个OIer……看过的比这恶心的代码多了去了……
引用
评论
15年11个月前IP:未同步
280877
2楼
[s:222]讲的好 学习了
引用
评论
15年11个月前IP:未同步
280886
3楼
看看IOCCC那些神人写的代码,比如这个


#include <stdio.h>
char *a;int M(int t,int _,char*a){
return!0<t?t<3?M(-79,-13,a+M(-87,1-_,M(-86,0,a+1)+a)):
1,t<_?M(t+1,_,a):3,M(-94,-27+t,a)&&t==2?_<13?
M(2,_+1,"%s %d %d\\n"):9:16:t<0?t<-72?M(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \\
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \\
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \\
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \\
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')#\\
}'+}##(!!/"):t<-50?_==*a?putchar(31[a]):M(-65,_,a+1):M((*a=='/')+t,_,a+1)
:0<t?M(2,2,"%s"):*a=='/'||M(0,M(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);}
int main(){M(1,0,0);getchar();}

运行结果吓死你
引用
评论
15年11个月前IP:未同步
280889
4楼
回 3楼(cqsrmxxzyx) 的帖子
写了一段密码?
引用
评论
15年10个月前IP:未同步
285321
5楼
我感觉这代码我自己想的话想不出来,非常的巧妙,但是一看就感觉原理很简单,很容易理解.看的时候很容易,我自己想想不出来的那种.
引用
评论
15年8个月前IP:未同步
293973
6楼
回 3楼(cqsrmxxzyx) 的帖子
记得以前看过某本小说有句话大意是这样,真正的高手写出来的代码都是丑陋无比的~~~
PS:我几乎要以为是乱码了~~~~
引用
评论
15年8个月前IP:未同步
294055
7楼
Re:回 3楼(cqsrmxxzyx) 的帖子
对一个软件开发人员最高的赞美是别人能看懂他写的代码
引用第6楼yj19920705于2011-05-10 20:52发表的 回 3楼(cqsrmxxzyx) 的帖子 :
记得以前看过某本小说有句话大意是这样,真正的高手写出来的代码都是丑陋无比的~~~
PS:我几乎要以为是乱码了~~~~
引用
评论
15年8个月前IP:未同步
294079
8楼
引用第3楼cqsrmxxzyx于2011-02-10 14:03发表的  :
看看IOCCC那些神人写的代码,比如这个


#include <stdio.h>
char *a;int M(int t,int _,char*a){
.......

给个解释好吧?
引用
评论
14年1个月前IP:未同步
474188
9楼
在这里使用8个来分组 是 因为 不管/8 还是 %8 在编译解释的时候都是转成 对应的位操作。 /8是 右移3位,%8是和0x07按位与,这是机器码又短运行速度又快的写法。同理 如果加大分组 16,32,......。
其实稍微折中一下 完全可以写得 相对容易懂 效率又高的。 先把不足分组的 用老方式 复制,剩下满分组的 再用小组为单位复制,这样的程序 效率和原来的写法没有多少区别,但是可读性大大提高。
引用
评论
笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的