Duff的设备如何工作?


Answers:


240

在其他地方有一些很好的解释,但让我尝试一下。(这在白板上要容易得多!)这是带有一些符号的Wikipedia示例。

假设您要复制20个字节。第一步的程序流控制为:

int count;                        // Set to 20
{
    int n = (count + 7) / 8;      // n is now 3.  (The "while" is going
                                  //              to be run three times.)

    switch (count % 8) {          // The remainder is 4 (20 modulo 8) so
                                  // jump to the case 4

    case 0:                       // [skipped]
             do {                 // [skipped]
                 *to = *from++;   // [skipped]
    case 7:      *to = *from++;   // [skipped]
    case 6:      *to = *from++;   // [skipped]
    case 5:      *to = *from++;   // [skipped]
    case 4:      *to = *from++;   // Start here.  Copy 1 byte  (total 1)
    case 3:      *to = *from++;   // Copy 1 byte (total 2)
    case 2:      *to = *from++;   // Copy 1 byte (total 3)
    case 1:      *to = *from++;   // Copy 1 byte (total 4)
           } while (--n > 0);     // N = 3 Reduce N by 1, then jump up
                                  //       to the "do" if it's still
    }                             //        greater than 0 (and it is)
}

现在,开始第二遍,我们只运行指示的代码:

int count;                        //
{
    int n = (count + 7) / 8;      //
                                  //

    switch (count % 8) {          //
                                  //

    case 0:                       //
             do {                 // The while jumps to here.
                 *to = *from++;   // Copy 1 byte (total 5)
    case 7:      *to = *from++;   // Copy 1 byte (total 6)
    case 6:      *to = *from++;   // Copy 1 byte (total 7)
    case 5:      *to = *from++;   // Copy 1 byte (total 8)
    case 4:      *to = *from++;   // Copy 1 byte (total 9)
    case 3:      *to = *from++;   // Copy 1 byte (total 10)
    case 2:      *to = *from++;   // Copy 1 byte (total 11)
    case 1:      *to = *from++;   // Copy 1 byte (total 12)
           } while (--n > 0);     // N = 2 Reduce N by 1, then jump up
                                  //       to the "do" if it's still
    }                             //       greater than 0 (and it is)
}

现在,开始第三遍:

int count;                        //
{
    int n = (count + 7) / 8;      //
                                  //

    switch (count % 8) {          //
                                  //

    case 0:                       //
             do {                 // The while jumps to here.
                 *to = *from++;   // Copy 1 byte (total 13)
    case 7:      *to = *from++;   // Copy 1 byte (total 14)
    case 6:      *to = *from++;   // Copy 1 byte (total 15)
    case 5:      *to = *from++;   // Copy 1 byte (total 16)
    case 4:      *to = *from++;   // Copy 1 byte (total 17)
    case 3:      *to = *from++;   // Copy 1 byte (total 18)
    case 2:      *to = *from++;   // Copy 1 byte (total 19)
    case 1:      *to = *from++;   // Copy 1 byte (total 20)
           } while (--n > 0);     // N = 1  Reduce N by 1, then jump up
                                  //       to the "do" if it's still
    }                             //       greater than 0 (and it's not, so bail)
}                                 // continue here...

现在将复制20个字节。

注意:原始Duff的设备(如上所示)已复制到该to地址处的I / O设备。因此,不必增加指标*to。在两个内存缓冲区之间复制时,需要使用*to++


1
如何跳过case 0:子句,并继续检查do while循环内的其他子句(已跳过子句的参数)?如果跳过了do while循环之外的唯一子句,为什么开关不在那里结束?
Aurelius

14
不要这么用力看括号。不要看do那么多。取而代之的是,查看带有偏移量的switchwhile老式计算GOTO语句或汇编器jmp语句。进行switch一些数学运算,然后jmp将其放置在正确的位置。在while做一个布尔检查,然后盲目jmps到右约在do了。
克林顿·皮尔斯

如果太好了,为什么每个人都不用呢?有什么缺点吗?
AlphaGoku

@AlphaGoku可读性。
LF

108

Dobb博士日记中解释是我在该主题上发现的最好的解释

这是我的AHA时刻:

for (i = 0; i < len; ++i) {
    HAL_IO_PORT = *pSource++;
}

变成:

int n = len / 8;
for (i = 0; i < n; ++i) {
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
    HAL_IO_PORT = *pSource++;
}

n = len % 8;
for (i = 0; i < n; ++i) {
    HAL_IO_PORT = *pSource++;
}

变成:

int n = (len + 8 - 1) / 8;
switch (len % 8) {
    case 0: do { HAL_IO_PORT = *pSource++;
    case 7: HAL_IO_PORT = *pSource++;
    case 6: HAL_IO_PORT = *pSource++;
    case 5: HAL_IO_PORT = *pSource++;
    case 4: HAL_IO_PORT = *pSource++;
    case 3: HAL_IO_PORT = *pSource++;
    case 2: HAL_IO_PORT = *pSource++;
    case 1: HAL_IO_PORT = *pSource++;
               } while (--n > 0);
}

好帖子(加上我必须从您那里找到一个很好的答案才能投票;)2下,13继续:stackoverflow.com/questions/359727#486543)。享受精美的答案徽章。
VonC

13
关键的事实是使Duff的设备在最长的时间里令我难以理解,这是由于C的怪癖,在它第一次到达时,它跳回并执行了所有语句。因此,即使len%8是4,它也将执行情况4,情况2,情况2和情况1,然后从下一个循环开始跳回并执行所有情况。这是需要说明的部分,循环和switch语句“交互”的方式。
ShreevatsaR 2012年

2
Dobbs博士的文章很好,但是除了链接之外,答案没有添加任何内容。请参见下面的Rob Kennedy的答案,该答案实际上提供了一个重要的知识点,即首先处理剩余的传输大小,然后再处理零个或多个8字节的传输块。我认为这是理解此代码的关键。
理查德·钱伯斯

3
我是否缺少某些内容,或者第二个代码段中的len % 8字节不会被复制?
2014年

我被困住了,忘记了,如果您没有在案例的语句列表的末尾写一个break语句,C(或任何其他语言)将继续执行这些语句。因此,如果您想知道duff的设备为何能正常工作,那么这是其中的关键部分
goonerify 2014年

75

Duff的设备有两个关键方面。首先,我怀疑这是较容易理解的部分,因此展开了循环。通过避免检查循环是否完成并跳回到循环顶部所涉及的一些开销,这可以以更大的代码大小换取更快的速度。当执行直线代码而不是跳跃时,CPU可以运行得更快。

第二个方面是switch语句。它允许代码在第一次通过时跳入循环的中间。对于大多数人来说,令人惊讶的部分是允许这种事情。好吧,这是允许的。执行开始于计算的情况下的标签,然后将其下落通过对每个连续赋值语句,就像任何其他的switch语句。在最后一个case标签之后,执行到达循环的底部,然后它跳回到顶部。循环的顶部是内部 switch语句,因此交换机不再被重新评估。

原始循环展开八次,因此迭代次数除以八。如果要复制的字节数不是8的倍数,则剩余一些字节。大多数一次复制字节块的算法将在末尾处理其余字节,但Duff的设备在开始时处理它们。该函数count % 8为switch语句计算出余数,然后跳转到该字节数的case标签,然后复制它们。然后循环继续复制八个字节的组。


5
这种解释更有意义。我了解的关键是先复制其余部分,然后其余部分以8字节为单位进行复制,这是不寻常的,因为如大多数时候所述,您将以8字节的块进行复制,然后复制其余部分。首先完成其余部分是了解此算法的关键。
理查德·钱伯斯

+1表示疯狂放置/嵌套开关/ while循环。不可能想象来自Java之类的语言...
Parobay 2014年

13

Duffs设备的重点是减少在紧凑的内存实现中进行的比较次数。

假设您要将“计数”字节从a复制到b,简单的方法是执行以下操作:

  do {                      
      *a = *b++;            
  } while (--count > 0);

您需要比较多少次以查看其是否大于0?“计数”时间。

现在,duff设备使用开关盒的令人讨厌的意外副作用,这使您可以减少计数/ 8所需的比较次数。

现在假设您要使用duffs设备复制20个字节,您需要进行多少次比较?仅3个,因为您一次复制8个字节,但最后一个第一个字节仅复制4个字节。

更新:您不必进行8个比较/切换中的语句,但是在函数大小和速度之间进行权衡是合理的。


3
请注意,在switch语句中,duff的设备不限于8个重复项。
斯特拉格

您为什么不能只使用--count,count = count-8来代替?并使用第二个循环来处理剩余部分?
hhafez

1
哈菲兹,您可以使用第二个循环来处理剩余部分。但是现在,您可以在不增加速度的情况下完成相同任务的代码增加一倍。
罗伯·肯尼迪

约翰,你落后了。剩余的4个字节在循环的一次迭代中复制,而不是最后一次。
罗伯·肯尼迪

8

第一次阅读时,我将其自动格式化为

void dsend(char* to, char* from, count) {
    int 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:

  • 语言定义中对switch语句的宽松规范。在设备发明之时,这是The C Programming Language的第一版,它仅要求switch的受控语句是句法有效(复合)语句,在这种情况下,大小写可以出现在任何子语句之前。结合以下事实:在没有break语句的情况下,控制流将从一个case标签所控制的语句转到另一个case标签所控制的语句,这意味着该代码指定了从顺序源地址到内存映射的输出端口。
  • 合法地跳入C循环中间的能力。

6

1:Duffs设备是循环展开的特殊体现。什么是循环展开?
如果您有一个循环执行N次的操作,则可以通过执行N / n次循环,然后在循环内联(展开)循环代码n次中来交换程序大小,以换取速度,例如,替换:

for (int i=0; i<N; i++) {
    // [The loop code...] 
}

for (int i=0; i<N/n; i++) {
    // [The loop code...]
    // [The loop code...]
    // [The loop code...]
    ...
    // [The loop code...] // n times!
}

如果N%n == 0,那么效果很好-不需要Duff! 如果那是不正确的,那么您必须处理其余部分-这很痛苦。

2:Duffs设备与此标准循环展开有何不同?
当N%n!= 0时,Duffs设备只是处理其余循环的一种聪明方法。整个do / while根据标准循环展开执行N / n次(因为适用0的情况)。在循环的最后一次运行中(第N / n + 1次),案例开始执行,我们跳到N%n案例,并以“剩余”次数运行循环代码。


在以下问题之后,我对Duffs设备产生了兴趣:stackoverflow.com/questions/17192246/switch-case-weird-scoping, 所以我认为Id可以澄清Duff-不知道对现有答案是否有任何改善...
Ricibob

3

虽然我不确定您要什么,但还是不能100%确定...

Duff的设备地址的问题是循环展开的问题之一(正如您无疑会在发布的Wiki链接上看到的那样)。这基本上等于在内存占用空间上优化运行时效率。Duff的设备处理串行复制,而不仅仅是任何旧问题,而是一个经典示例,说明如何通过减少循环中进行比较的次数来进行优化。

作为一个替代示例,它可能会使您更容易理解,假设您有一个要循环的项目数组,并且每次都向其中添加1 ...通常,您可以使用for循环,并循环大约100次。这似乎很合逻辑,但是...可以通过展开循环来进行优化(显然不会太远……或者您也可以不使用循环)。

因此,一个常规的for循环:

for(int i = 0; i < 100; i++)
{
    myArray[i] += 1;
}

变成

for(int i = 0; i < 100; i+10)
{
    myArray[i] += 1;
    myArray[i+1] += 1;
    myArray[i+2] += 1;
    myArray[i+3] += 1;
    myArray[i+4] += 1;
    myArray[i+5] += 1;
    myArray[i+6] += 1;
    myArray[i+7] += 1;
    myArray[i+8] += 1;
    myArray[i+9] += 1;
}

Duff的设备所做的就是用C语言实现这个想法,但是(如您在Wiki上看到的)使用串行副本。在上面的示例中,您看到的是10个比较,而原始比较为100个,这是次要的,但可能是重大的优化。


8
您缺少关键部分。这不仅仅是关于循环展开。switch语句跳入循环的中间。这就是使设备看起来如此混乱的原因。上面的循环始终执行10倍的倍数,但是Duff可以执行任意数目。
罗伯·肯尼迪

2
是的,但是我试图简化OP的描述。也许我还不够清楚!:)
James B

2

这是一个非详细的解释,我认为这是Duff设备的症结所在:

事实是,C基本上是汇编语言的一个不错的门面(具体来说是PDP-7汇编;如果您研究发现,将发现惊人的相似性)。而且,在汇编语言中,您实际上没有循环-您具有标签和条件分支指令。因此,循环只是整个指令序列的一部分,带有标签和分支的位置:

        instruction
label1: instruction
        instruction
        instruction
        instruction
        jump to label1  some condition

并且switch指令在某种程度上向前跳转/跳转:

        evaluate expression into register r
        compare r with first case value
        branch to first case label if equal
        compare r with second case value
        branch to second case label if equal
        etc....
first_case_label: 
        instruction
        instruction
second_case_label: 
        instruction
        instruction
        etc...

在汇编中,很容易想到如何组合这两个控制结构,以这种方式考虑时,它们在C中的组合似乎不再那么奇怪了。


1

这是我在另一个有关Duff装置的问题上发布的答案,该问题在作为重复问题被关闭之前得到了一些好评。我认为这为您为什么应避免使用此构造提供了一些有价值的背景信息。

“这是Duff的设备。这是展开循环的一种方法,避免了必须添加辅助修复循环来处理循环迭代次数不知道是展开系数的确切倍数的时间。

由于这里的大多数答案似乎普遍都是正面的,因此我将着重指出其不利之处。

使用此代码,编译器将难以将任何优化应用于循环体。如果您只是将代码编写为简单循环,那么现代编译器应该可以为您处理展开。这样,您可以保持可读性和性能,并希望将其他优化应用于循环体。

其他人引用的Wikipedia文章甚至说,当从Xfree86源代码中删除此“模式”时,性能实际上得到了改善。

这种结果通常是盲目地优化您碰巧认为可能需要的任何代码。它会阻止编译器正确执行其工作,使您的代码可读性更差,更容易出现错误,并且通常会使速度变慢。如果您一开始就以正确的方式做事,例如编写简单的代码,然后进行瓶颈分析,然后进行优化,则您甚至都不会想到使用这种方法。无论如何,现代的CPU和编译器都没有。

理解它很好,但是如果您实际使用它,我会感到惊讶。”


0

只是做实验,发现另一个变种相处而又不交错切换和循环:

int n = (count + 1) / 8;
switch (count % 8)
{
    LOOP:
case 0:
    if(n-- == 0)
        break;
    putchar('.');
case 7:
    putchar('.');
case 6:
    putchar('.');
case 5:
    putchar('.');
case 4:
    putchar('.');
case 3:
    putchar('.');
case 2:
    putchar('.');
case 1:
    putchar('.');
default:
    goto LOOP;
}

您的终止条件在哪里?
user2338150
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.