在电脑科学领域,达夫装置(英文: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]。
参见
参考资料
外部链接
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.