【问题标题】:How can Duff's device code be compiled?Duff 的设备代码如何编译?
【发布时间】:2011-04-06 15:58:14
【问题描述】:

我明白为什么Duff's device 比可以展开但未优化的普通循环代码更快。但我还不明白如何编译代码。
我想这是关于 switch 语法的一个技巧。但现在不是了。

switch句中怎么能存在do while句?很奇怪。
有谁可以解释一下吗?

编辑: 另一个问题。 为什么 duff 使用 8?它可以是 16、65536 或其他。因为代码大小?还有别的原因吗?例如,缓存或流水线的好处。

【问题讨论】:

  • “我明白为什么 Duff 的设备比没有优化的普通代码要快。”它不是。它可能只会让编译器和人类一样感到困惑,从而可能导致更慢代码。即使对 Duff 的设备有不同的支持,你也不需要如此糟糕的速度,你需要使用这种可憎的东西。跟我重复一遍。你不需要使用这个可憎的。
  • Duff 的设备 过去 比普通代码快(至少在某些平台上)——现在几乎没有了。在大约 10 年前我们在 Windows PC 和 MSVC 上进行的一个项目中,结果证明它比优化的普通代码
  • 真正的教训是相信你的编译器。不要以牺牲可读性和可维护性为代价来追逐性能边际收益的可能性(即使在这种情况下也是如此)。任何时候你必须就 SO 提出问题以理解一段代码或让它编译,你可能不应该尝试使用它。
  • @Hogan - 您是否声称 C 和 C++ 之间存在一些影响 Duffs 设备的差异?我能想到的唯一一件事就是与在开关/循环中声明的局部变量的范围、构造和销毁有关,这几乎肯定不是问题。
  • IMO 问题不在于 Duff 的设备,而是您不应该担心的任何类型的手动循环展开。在那些非常罕见的情况下,当您确实手动展开循环以查看它是否优于优化器(有时会这样做)时,您的循环可能不是展开程度的倍数。此时,您可能会选择使用 Duff 的设备,而不是一堆 goto,正是因为它可以让编译器优化操作的选择和跳转部分。不过,我不明白为什么人们会对有人能走到这一步的想法如此震惊。

标签: c++ c switch-statement duffs-device


【解决方案1】:

“它是如何工作的”很简单。

C 和 C++ 都是编译语言,通常编译为平台机器码。机器代码没有块结构的概念 - 所有块结构都必须转换为使用(实际上)无条件和条件 goto 的某种组合的形式。

C 语法规则允许 switch 语句和循环以一种不是真正的分层块结构的方式组合,但它会缠结控制流。只要编译器可以处理这个(任何好的编译器都应该),底层机器代码就没有问题。结果将是“意大利面条”,但通过优化器生成的机器代码始终是意大利面条——它并不意味着人类可读,所以这不是问题。这里的问题是源代码也是意大利面条,即使 goto 被“隐藏”了。

注意 - 虽然任何好的编译器都应该能够处理 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 标签没有什么不同,如上所述。

警告 - 这纯粹是为了说明一个想法,不应在现实生活中复制代码。

【讨论】:

  • 顺便说一句,并非所有编译器都能从 Duff 的设备生成正确的代码。如果您尝试使用 C++ Builder 编译它,则需要禁用称为“启用循环感应变量和强度降低”的优化,否则您将在运行时遇到访问冲突。
【解决方案2】:

对 Duff 的 Device 编译原因的简单解释是,switch 语句的语法对于switch 语句块可能需要采用的形式并不特别具体。在受控语句中有一些限制,以及在 switchcasedefault 标签)之外不允许的几件事。但除此之外,受控语句只是任何其他语句,可能有标签供switch 为目标。

这是来自 C99 的语法:

switch ( expression ) statement

除了语法之外,该标准还施加了一些限制:

  • 控制表达式必须是整数类型
  • VLA 可以在 switch 语句中出现的位置受到限制
  • case 标签表达式必须是整数常量表达式
  • case 标签表达式或default 标签不能重复

除此之外,语句块中允许的任何构造都应允许在受控语句中(除了casedefault 标签是可以的)。请记住,casedefault 只是开关根据控制表达式和case 标签表达式跳转到的标签。 As Potatoswatter says, switch 只是一个计算出来的goto。所以就像goto 可以跳到循环的中间一样,switch 也可以。

此外,我认为您可能会从 Duff 的设备中受益的情况在今天非常罕见(我认为即使在 1980 年代也很少见)。别忘了汤姆·达夫本人在他的description 中说过:

  • “恶心,不是吗?”
  • “我对这一发现感到既自豪又厌恶。”

与最初描述时相比,Duff 的设备应该被视为一种好奇心而不是使用的工具。

【讨论】:

  • Tom duff 的描述很有趣。我没读过。谢谢。
【解决方案3】:

switch 只是一个计算出来的goto。因此,循环内有几个标签,循环外有一个switch 语句。 switch 决定要转到哪个标签,gotos 在那里,在循环内。

一旦执行进入循环,它就会继续循环,直到循环放弃控制。

它实际上非常简单……除非是最直接的替代方法,否则不应使用它。

不要相信任何告诉你它可以使任何事情变得快速的人。

我什至会说不要听他们说的任何话根本,即使是你的老师。

至于编译器,他们将事情分解为通用控制流图,而不关心 switchifwhile。对编译器来说都是if ( … ) goto …; else goto …;

【讨论】:

    【解决方案4】:

    虽然 Duff 的设备就其最初的用途而言确实已经过时,但它仍可用于特殊用途,例如通常在 N 状态中反复循环的状态机,但有时需要返回给调用者并稍后在它停止的状态。将switch 语句放在循环外,将case 标签放在循环内(我认为这是“Duff 设备”的定义)就很有意义了。

    话虽如此,请不要使用 Duff 的设备“手动优化”。将有效的“goto 标签”放在所有地方不会帮助编译器优化。

    【讨论】:

      【解决方案5】:

      如果我们从您链接的维基百科文章中获取实现...

      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 范围没有引入任何局部变量的这种用法中 - do-while 真的没有什么比跳回绕过开关的 case 0 更重要的了因此需要评估 count % 8 (% 在事物方案中是一个非常昂贵的操作)。

      希望能帮助它点击,但可能不会...? :-)

      为什么 duff 使用 8?它可以是 16、65536 或其他。因为代码大小?还有别的原因吗?例如,缓存或流水线的好处。

      只是收益递减的一个例子。必须进行--n > 0 检查并在每 8 个数据副本后跳转并不是很大的开销,但代码的大小(源代码和缓存中的编译代码)仍然非常紧凑。可能是 90% 或 95% 的工作与间接费用,这显然已经足够了。此外,为了说明和与其他人分享这个概念,Tom Duff 可能更喜欢它是一个典型的 80x25 终端的代码价值,而不是一页或 10 页。

      【讨论】:

        猜你喜欢
        • 2011-01-27
        • 1970-01-01
        • 1970-01-01
        • 2010-10-05
        • 1970-01-01
        • 2010-12-15
        • 2010-11-29
        • 1970-01-01
        相关资源
        最近更新 更多