跳转到导航跳转在C编程语言中,Duff的设备是一种通过交织C的两个语法结构(Do-While循环和Switch语句)手动实现循环展开的方法。它的发现归功于汤姆·达夫(Tom Duff)在1983年11月,当时达夫正在为卢卡斯电影公司工作,并用它来加速一个实时动画程序。
循环展开试图通过每次迭代执行一批循环体来减少检查循环是否完成所需的条件分支的开销。为了处理迭代数不能被展开的循环增量整除的情况,汇编语言程序员的一种常见技术是直接跳到展开的循环体的中间来处理剩余的部分。[1]达夫使用C';的案例标签跌落特性跳入展开的主体,从而用C语言实现了这项技术。[2]。
Duff';的问题是将16位单元(在大多数C实现中是短码)从数组复制到内存映射输出寄存器中,在C中用指针表示。他用C语言编写的原始代码如下所示:[3][4]。
发送(To,From,Count)寄存器短*To,*From;寄存器计数;{do{/*count>;0假定*/*to=*From++;}While(--count>;0);}。
此代码假设初始计数>;0。指向的指针不会像内存到内存复制所需的那样递增。如果Count始终可以被8整除,则将此循环展开8倍将产生以下结果:
发送(收件人,发件人,计数)寄存器短*收件人,*发件人;寄存器计数;{寄存器n=计数/8;做{*收件人=*发件人++;*收件人=*发件人++;}While(--n>;0);}。
达夫意识到,为了处理count不能被8整除的情况,汇编程序员跳入循环体的技术可以通过交错switch语句和循环的结构来实现,将Switch的case标签放在循环体中与count/8:[1]的其余部分相对应的点上。
发送(收件人,发件人,计数)寄存器短*收件人*收件人,*发件人;寄存器计数;{寄存器n=(计数+7)/8;开关(计数%8){案例0:DO{*收件人=*发件人++;案例7:*收件人=*发件人++;案例6:*收件人=*发件人++;案例5:*收件人=*发件人++;案例4:*收件人=*发件人++;案例4:*收件人=*发件人++;案例3:*To=*From++;案例2:*To=*From++;案例1:*To=*From++;}While(--n>;0);}}
Duff';的设备可以类似地应用于展开的环的任何其他尺寸,而不仅仅是上面例子中的8个。
基于汇编语言编程人员广泛使用的一种算法,以最小化复制过程中的测试和分支数量,Duff&39;的设备在用C实现时显得格格不入。该设备是有效的C,这得益于C中的两个属性:
在语言的定义中放松了对switch语句的规范。在设备发明的时候,这是C编程语言的第一个版本,它只要求开关的主体是句法上有效的(复合)语句,其中case标签可以出现在任何子语句的前缀。在没有BREAK语句的情况下,控制流将从一个CASE标签控制的语句下降到由下一个CASE标签控制的语句,这意味着代码指定了从连续的源地址到内存映射输出端口的一系列计数副本。
这导致了行话文件所说的在C&34;中迄今见过的最戏剧性的对Fall的使用。[5]C&39;Case语句的缺省缺陷长期以来一直是其最具争议的特性之一;达夫本人表示,这一代码在这场辩论中形成了某种争论,但我不确定它是赞成还是反对。[5]。
发送(收件人,发件人,计数)寄存器短*收件人*收件人,*发件人;寄存器计数;{寄存器n=(计数+7)/8;开关(计数%8){案例0:*收件人=*发件人++;案例7:*收件人=*发件人++;案例6:*收件人=*发件人++;案例5:*收件人=*发件人++;案例4:*收件人=*发件人++;案例3:*收件人=*发件人++;案例3:*收件人=*发件人++;案例2:*To=*From++;案例1:*To=*From++;}While(--n>;0){*To=*From++;*To=*From++;}}。
循环展开的基本思想是,可以通过减少循环测试的数量(有时减少在循环中花费的时间)来减少在循环中执行的指令数量。例如,在块代码中只有一条指令的循环的情况下,通常将针对循环的每次迭代,即每次执行指令时执行循环测试。相反,如果将同一指令的8个副本放入循环中,则测试将仅每8次迭代执行一次,这可以通过避免7次测试来赢得时间。然而,这只处理8个迭代的倍数,需要其他东西来处理其余的迭代。[1]。
达夫的设备提供了一种解决方案,首先执行剩余的迭代,然后根据需要重复八条类似指令的倍数。为了确定余数迭代次数,代码首先计算以8为模的迭代总数。根据这个剩余部分,程序执行将跳转到CASE语句,后面紧跟所需的迭代次数。一旦这样做了,一切都很简单-代码继续进行八个指令组的迭代,这已经成为可能,因为剩余的迭代次数是八的倍数。[1]。
Duff';的设备通过在循环内部和外部使用case关键字来提供紧凑的循环展开。这是不寻常的,因为CASE语句的内容传统上被认为是嵌套在CASE语句中的代码块,读者通常希望它在下一个CASE语句之前结束。根据C语言的规范,这是不必要的;实际上,case语句可以出现在开关代码块内的任何位置,并且可以出现在任何深度;程序执行将简单地跳到下一条语句,无论它在哪里。
许多编译器会将开关优化为分支表,就像在汇编实现中所做的那样。
与简单、直接的循环相比,速度的主要提高来自循环展开,它减少了执行的分支的数量,由于需要刷新-并因此停止-指令流水线,因此执行的分支的数量在计算上是昂贵的。SWITCH语句用于处理不能被展开的操作数整除的剩余数据(在本例中,将展开8个字节的移动,因此交换机会自动处理额外的1-7个字节)。
这种对剩余部分的自动处理可能并不是所有系统和编译器上的最佳解决方案-在某些情况下,两个循环实际上可能更快(一个循环,展开以进行主复制,第二个循环来处理剩余部分)。问题似乎归结为编译器正确优化设备的能力;它还可能会干扰某些体系结构上的流水线和分支预测。[6]在4.0版的XFree86服务器中删除了Duff';设备的多个实例后,性能得到了提高,可执行文件的大小也明显减小。[7]因此,当将此代码视为程序优化时,可能值得运行几个基准测试,以验证它实际上是目标体系结构上目标优化级别上使用目标编译器最快的代码,并权衡优化后的代码稍后将在不同平台上使用的风险,在这些平台上,它不是最快的代码。
出于内存到内存复制的目的(如上所述,这不是Duff的设备的原始用途),标准C库提供函数memcpy;[8]它的性能不会比此代码的内存到内存复制版本差,并且可能包含特定于体系结构的优化,使其速度大大提高。(=。[9][10]。
^a b c d Holly,拉尔夫(2005年8月1日)。";一种可重复使用的达夫设备";。多布博士的杂志。
“左,泰德”(2000年8月22日)。";Re:[补丁]Re:输入驱动程序的移动,您需要一些单词。1kml.Indiana.edu。Linux内核邮件列表。Jim Gettys在X服务器中对此效应进行了精彩的解释。事实证明,随着分支预测和CPU与内存的相对速度在过去十年中不断变化,循环展开几乎没有意义。事实上,通过从XFree86 4.0服务器中删除Duff设备的所有实例,服务器的大小缩小了_一半_兆字节_(!),并且启动速度更快,因为消除了所有多余的代码意味着X服务器不会对高速缓存线造成太大的冲击。
^Fog,Agner(2012-02-29)。用汇编语言优化子例程(PDF)。哥本哈根大学工程学院。第100页以上。
科尼根,布莱恩;里奇,丹尼斯(1988年3月)。C编程语言(第二版)。新泽西州恩格尔伍德·克里夫斯:普伦蒂斯·霍尔(Prentice Hall)。ISBN电话:0-13-110362-8。
Adam Dunkels';ProtoThreadsC语言中的轻量级、无堆栈线程也使用嵌套的switch/case语句(另请参阅最轻的轻量级线程,ProtoThreadsTM)