达夫设备
在计算机科学领域,达夫设备(英文:Duff's device)是串行复制(serial copy)的一种优化实现,通过汇编语言编程时一常用方法,实现展开循环,进而提高执行效率。这一方法据信为当时供职于卢卡斯影业的汤姆·达夫于1983年11月发明,并可能是迄今为止利用C语言switch语句特性所作的最巧妙的实现。
优化背景
在编程时,循环展开注重于利用批量处理,减少总处理分支数。而在串行复制数据时,当总循环次数无法被展开后循环的增量整除时,一般就用直接跳转到展开后循环体中部的方式,完成零余数据的复制流程。
因此,根据循环展开的思想,针对串行复制数据的需要,汤姆·达夫以每次迭代时赋最多8
个值的方式,用C语言写出了一个优化实现,成功优化了串行复制的效率。
原版代码
若要将数组元素复制进存储器映射输出寄存器,较为直接的做法如下所示:
send(to, from, count)
register short *to, *from;
register count;
{
do { /* 假定了count > 0 */
*to = *from++;
} while (--count > 0);
}
注意这段代码所实现的并非“存储器到存储器”的复制,因而不需*to++
,采用循环展开将循环体展开为8
叠(eight-fold),代码如下:
send(to, from, count)
register short *to, *from;
register count;
{
register n = count % 8;
while (n-- > 0) {
*to = *from++;
}
n = count / 8;
if (n == 0) return;
do {
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
} while (--n > 0)
}
达夫洞察到可以利用上汇编程序员的跳转到循环体内的技术,这可以通过交错一条switch
语句和一个循环来实现,即把switch
的case
标记在循环体内对应于count/8
的余数的各个点上[1]:
send(to, from, count)
register short *to, *from;
register count;
{
register 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);
}
}
实现机制
达夫设备基于汇编语言编程中常用的“在复制时最小化判断数和分支数”所用算法,并以C语言实现。代码看起来虽与环境格格不入,但仍可与C语言相容,其因有二:
一方面,C语言对switch
语句的规范较松。达夫设备发明时,正值《C程序设计语言》第一版引领C语言规范,而按其中规定,在switch
控制语句内,条件标号(case
)可以出现在任意子语句之前,充作其前缀。另外,若未加入break
语句,则在switch
语句在根据条件判定,跳转到对应标号,并开始执行后,控制流会无视其余条件标号限定,一直执行到switch
嵌套语句的末尾,此即switch
语句的“穿透”(fallthrough)特性[2]。利用以上特性,这段代码便可从连续地址中将count
个数据复制到存储器映射输出寄存器中。
另一方面,C语言对跳转到循环内部提供了支持[3],因而此处的switch
/case
语句便可跳转到循环内部。
许多C语言编译器会仿效汇编语言的编程方式,将switch
语句转换为转移表,从而提高执行效率。在C语言中,switch
语句默认的“穿透”特性长期以来亦备受争议,而达夫也发觉,“这段代码成为了这一讨论中某些观点的论据,但我不确定到底是支持还是否定(这些观点)。”
性能表现
功能等价的版本switch 和while 不交错
|
---|
send(to, from, count)
register short *to, *from;
register count;
{
register n = count / 8;
switch (count % 8) {
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++;
case 0: ;
}
if (n == 0) return;
do {
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
} while (--n > 0)
}
|
从速度上说,由于采用了循环展开技巧,使所需处理的分支数减少,从而降低了在处理分支时,中断与刷新流水线的巨大运算开销,因而相较于简单、直接的循环代码,这段代码的执行效率较高。另外,由代码易知,若不带switch
语句,则这段代码只能复制8*n
个数据项,而若count
无法为8
整除,则仍有count%8
(即count
除于8
的余数)项未处理;有鉴于此,此间嵌入了switch
/case
语句,负责处理零余数据。
但是,达夫设备亦有其局限性。在某些环境下,利用switch
/case
语句处理零余数据项,有时并非最优选择;相对应的,若采用双循环机制可能反倒更快(实现一个展开后的循环复制8*n
个数据项,和另一循环复制零余数据项)。究其肇因,则常是由于编译器无法针对达夫设备进行优化,但亦可能是因某些架构的流水线与分支预测机制有所差异[4]。除此以外,据测试来看,当从XFree86 Server 4.0代码中清理掉所有达夫设备代码后,执行效能却大幅提升[5]。因此,若打算使用达夫设备,最好先针对所用的硬件架构、优化等级和编译器对达夫设备进行基准测试,而后再做定夺。
斯特劳斯鲁普版代码
原版的达夫设备仅能满足将数据复制到一个(存储器映射)寄存器的需求。若要在存储地址间串行复制数据,则在每次引用指针to
时,都需进行一次自增操作,如下所示:
*to++ = *from++;
此版代码摘自比雅尼·斯特劳斯特鲁普著《C++程序设计语言》一书的“这段代码有何用?(what does this code do?)”练习部分,而他之所以如此修改,很可能是因考虑到编程新手一般对存储器映射输出寄存器一无所知。值得一提的是,针对串行复制的需求,标准C语言库提供了memcpy函数,而其效率不会比斯特劳斯鲁普版的达夫设备低,并可能包含了针对特定架构的优化,从而进一步大幅提升执行效率[6][7]。
参见
参考资料
本條目部分或全部内容出自以GFDL授權發佈的《自由線上電腦詞典》(FOLDOC)条目Duff's device。
- ^ Holly, Ralf. A Reusable Duff Device. Dr. Dobb's Journal. August 1, 2005 [September 18, 2015]. (原始内容存档于2020-02-26).
- ^ Brian Kernighan, Dennis Ritchie. The C Programming Language, Second Edition (PDF). Prentice Hall. 1988 [2024-01-19]. (原始内容存档 (PDF)于2023-03-25).
The
switch
statement is a multi-way decision that tests whether an expression matches one of a number of constant integer values, and branches accordingly. ……
Each case is labeled by one or more integer-valued constants or constant expressions. If a case matches the expression value, execution starts at that case. ……
Because cases serve just as labels, after the code for one case is done, execution falls through to the next unless you take explicit action to escape. ……
Case labels and default labels are used with theswitch
statement (Par.A.9.4). ……
Labels themselves do not alter the flow of control. - ^ Brian Kernighan, Dennis Ritchie. The C Programming Language, Second Edition (PDF). Prentice Hall. 1988 [2024-01-19]. (原始内容存档 (PDF)于2023-03-25).
A label has the same form as a variable name, and is followed by a colon. It can be attached to any statement in the same function as the
goto
. The scope of a label is the entire function. ……
Because labels have their own name space, they do not interfere with other identifiers and cannot be redeclared. - ^ James Ralston's USENIX 2003 Journal[失效連結]
- ^ 曹子德. Re: [PATCH] Re: Move of input drivers, some word needed from you. Linux Kernel Archive Mailing List. [2013-07-08]. (原始内容存档于2014-01-08).
- ^ Wall, Mike. Using Block Prefetch for Optimized Memory Performance (PDF). mit.edu. 2002-03-19 [2012-09-22]. (原始内容存档 (PDF)于2017-08-30).
- ^ Fog, Agner. Optimizing subroutines in assembly language (PDF). Copenhagen University College of Engineering: 100 ff. 2012-02-29 [2012-09-22]. (原始内容存档 (PDF)于2021-03-29).
- ^ Simon Tatham. Coroutines in C. 2000 [2013-07-07]. (原始内容存档于2019-11-09).
外部链接
- 汤姆·达夫本人对达夫设备机制的解释 (页面存档备份,存于互联网档案馆)(英文)
- Duff's device (页面存档备份,存于互联网档案馆), C-FAQ.com(英文)
- A Reusable Duff Device (页面存档备份,存于互联网档案馆), Dr.Dobb's Journal(英文)