【转】古怪的“达夫设备”--快速数组复制法
warmonkey2011/02/02软件综合 IP:江西
原文:XXXXXXXXXXXXXXXXXXXt/nicky_zs/archive/2008/03/19/XXXXXXXXXXpx

前几天在网上看见了一段代码,叫做“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是优化过的,比自己写的要快很多~可能就是用这个代码)
来自:计算机科学 / 软件综合
10
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
ltl
14年0个月前 IP:未同步
278447
这个代码确实写得挺精妙的,而且很符合标准啊,do-while本来就是if-goto啊,不可能会编译错误……

作为一个OIer……看过的比这恶心的代码多了去了……
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
boldness123
13年11个月前 IP:未同步
280877
[s:222]讲的好 学习了
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
cqsrmxxzyx
13年11个月前 IP:未同步
280886
看看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();}

运行结果吓死你
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
boldness123
13年11个月前 IP:未同步
280889
回 3楼(cqsrmxxzyx) 的帖子
写了一段密码?
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
x_uy_u_n
13年10个月前 IP:未同步
285321
我感觉这代码我自己想的话想不出来,非常的巧妙,但是一看就感觉原理很简单,很容易理解.看的时候很容易,我自己想想不出来的那种.
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
城市迷彩_cc
13年8个月前 IP:未同步
293973
回 3楼(cqsrmxxzyx) 的帖子
记得以前看过某本小说有句话大意是这样,真正的高手写出来的代码都是丑陋无比的~~~
PS:我几乎要以为是乱码了~~~~
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
拔刀斋
13年8个月前 IP:未同步
294055
Re:回 3楼(cqsrmxxzyx) 的帖子
对一个软件开发人员最高的赞美是别人能看懂他写的代码
引用第6楼yj19920705于2011-05-10 20:52发表的 回 3楼(cqsrmxxzyx) 的帖子 :
记得以前看过某本小说有句话大意是这样,真正的高手写出来的代码都是丑陋无比的~~~
PS:我几乎要以为是乱码了~~~~
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
lovehongkong
13年8个月前 IP:未同步
294079
引用第3楼cqsrmxxzyx于2011-02-10 14:03发表的  :
看看IOCCC那些神人写的代码,比如这个


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

给个解释好吧?
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
zilingzhang
12年1个月前 IP:未同步
474188
在这里使用8个来分组 是 因为 不管/8 还是 %8 在编译解释的时候都是转成 对应的位操作。 /8是 右移3位,%8是和0x07按位与,这是机器码又短运行速度又快的写法。同理 如果加大分组 16,32,......。
其实稍微折中一下 完全可以写得 相对容易懂 效率又高的。 先把不足分组的 用老方式 复制,剩下满分组的 再用小组为单位复制,这样的程序 效率和原来的写法没有多少区别,但是可读性大大提高。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
warmonkey
学者 机友
文章
363
回复
7989
学术分
12
2008/10/11注册,4时13分前活动

Cubesat

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:未同步
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
支持的图片格式:jpg, jpeg, png
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}