我理解为什么Duff's device比正常循环代码更快,可以展开但未优化。但是我无法理解代码是如何编译的 我想这是关于 switch 语法的一个技巧。但不是了。
在开关句子中句子存在时怎么办?很奇怪。
有没有人可以解释这个?
修改 另一个问题。 为什么duff使用8?它可能是16,65536或其他什么。因为代码大小?还有其他原因吗?例如,缓存或流水线操作的好处。
答案 0 :(得分:11)
“它如何运作”很简单。
C和C ++都是编译语言,通常编译为平台机器代码。机器代码没有块结构的概念 - 所有块结构都必须转换为使用(实际上)某些无条件和条件gotos混合的形式。
C语法规则允许switch语句和循环以不是真正的分层块结构的方式组合,但是它会纠缠控制流。只要编译器可以处理这个(任何好的编译器都应该),底层机器代码就没有问题。结果将是“spaghetti”,但生成的机器代码通过优化器总是意大利面 - 它不是人类可读的,所以它不是问题。这里的问题是源代码也是spaghetti,即使gotos已被“隐藏”。
注意 - 尽管任何好的编译器都应该应对Duffs设备,正如其他人已经评论过的那样,但这并不意味着它能够很好地适应它以正确地优化它 - 只能很好地生成正确的可执行代码。这是曾经有目的的一些古老奇怪的习语,但现在更有可能混淆你的编译器并破坏它生成有效代码的能力。
修改强>
以下内容与Duffs设备有关,可能有助于说明基本思路......
switch (count & 1)
{
case 0 : goto lbl0;
case 1 : goto lbl1;
}
lbl0:
while (count != 0)
{
handle_one ();
count--;
lbl1:
handle_one ();
count--;
}
在循环中包含case子句在概念上与在循环中使用goto-target标签没有什么不同,如上所述。
警告 - 这纯粹是为了说明一个想法,不应该复制到现实代码中。
答案 1 :(得分:6)
为什么Duff的Device编译的一个简单解释是switch
语句的语法对于switch
语句块可能需要采用的形式并不特别具体。存在一些限制,受控语句中允许的一些限制在switch
(case
和default
标签)之外是不允许的。但除此之外,受控声明只是任何其他声明,可能存在switch
标记的目标。
以下是C99的语法:
switch ( expression ) statement
除了语法之外,标准还强加了一些约束:
case
标签表达式必须是整型常量表达式case
标签表达式或default
标签除此之外,应该在受控语句中允许语句块中允许的任何构造(添加case
和default
标签都可以)。请记住,case
和default
只是交换机根据控制表达式和case
标签表达式跳转到的标签。 As Potatoswatter says,switch
只是一个计算goto
。所以就像goto
可以跳到循环的中间一样,switch
也可以。
另外,我认为你可能会看到Duff设备的好处的情况今天非常罕见(我认为它们甚至在1980年代也很少见)。不要忘记Tom Duff自己在description中说了以下内容:
甚至比最初描述时更多,Duff的设备应该被视为一种好奇而不是一种工具。
答案 2 :(得分:4)
switch
只是一个计算goto
。因此,循环中有几个标签,循环外有一个switch
语句。 switch
决定在循环内转到哪个标签,goto
。
一旦执行在循环内部,它就会继续循环,直到循环放弃控制。
它实际上非常简单......除非它是最直接的选择,否则不应该使用它。
我甚至会说要停止听他们说的所有内容,即使这是你的老师。
对于编译器,他们将事情分解为通用控制流图,而不关心switch
与if
与while
的关系。它们都是编译器的if ( … ) goto …; else goto …;
。
答案 3 :(得分:2)
虽然Duff的设备因其原始目的而过时,但它仍然可用于特殊用途,例如通常通过N
状态重复循环的状态机,但有时需要返回调用者以后在它停止的州恢复。将switch
语句置于循环之外并将case
标签置于循环内(我将其视为“Duff的设备”的定义)然后很有意义。
话虽如此,不要使用Duff的设备“手动优化”。将所有有效的“转到标签”放在一起将无助于编译器进行优化。
答案 4 :(得分:2)
如果我们从维基百科文章中获取实施链接......
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);
}
}
...并将“高级”do
/ while
循环替换为汇编级if
/ goto
,编译器确实将其缩减为。 ..
send(to, from, count)
register short *to, *from;
register count;
{
register n=(count+7)/8;
switch(count%8){
case 0: do_label: *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++;
if (--n>0) goto do_label;
}
}
...它可能会帮助你认识到 - 在这种使用中,do / while范围没有引入任何局部变量 - 实际上没有什么比跳回到案例0绕过开关更多了因此需要评估计数%8(%是一个非常昂贵的操作方案)。
希望有助于它点击,但可能不会......? : - )
为什么duff使用8?它可能是16,65536或其他什么。因为代码大小?还有其他原因吗?例如,缓存或流水线操作的好处。
只是收益递减的情况。必须在每8个数据副本之后执行--n > 0
检查和跳转不是一个很大的百分比开销,但代码的大小(源代码和缓存中的编译代码)仍然非常紧张。也许这是90%或95%的工作与间接费用,这显然已经足够好了。此外,为了说明和与其他人分享这个概念,Tom Duff可能更喜欢它是一个典型的80x25终端的代码,而不是一个页面或10个。