达夫设备

计算机科学领域,达夫设备英文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语句和一个循环来实现,即把switchcase标记在循环体内对应于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语句默认的“穿透”特性长期以来亦备受争议,而达夫也发觉,“这段代码成为了这一讨论中某些观点的论据,但我不确定到底是支持还是否定(这些观点)。”

性能表现

功能等价的版本
switchwhile不交错
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]

参见

  • 协程  受达夫设备的影响,有人采用C/C++的“switch穿透”特性实现协程[8]
  • Jensen设备

参考资料

本條目部分或全部内容出自以GFDL授權發佈的《自由線上電腦詞典》(FOLDOC)条目Duff's device

  1. Holly, Ralf. . Dr. Dobb's Journal. August 1, 2005 [September 18, 2015]. (原始内容存档于2020-02-26).
  2. Brian Kernighan, Dennis Ritchie. (PDF). Prentice Hall. 1988. 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 the switch statement (Par.A.9.4). ……
    Labels themselves do not alter the flow of control.
  3. Brian Kernighan, Dennis Ritchie. (PDF). Prentice Hall. 1988. 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.
  4. James Ralston's USENIX 2003 Journal
  5. 曹子德. . Linux Kernel Archive Mailing List. [2013-07-08]. (原始内容存档于2014-01-08).
  6. Wall, Mike. (PDF). mit.edu. 2002-03-19 [2012-09-22]. (原始内容存档 (PDF)于2017-08-30).
  7. Fog, Agner. (PDF). Copenhagen University College of Engineering: 100 ff. 2012-02-29 [2012-09-22]. (原始内容存档 (PDF)于2021-03-29).
  8. Simon Tatham. . 2000 [2013-07-07]. (原始内容存档于2019-11-09).

外部链接

This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.