Duff的设备如何工作?

时间:2009-02-05 01:06:05

标签: c duffs-device

我读过article on Wikipedia on the Duff's device,但我没理解。我真的很感兴趣,但我已经在那里读过几次解释,但我仍然不知道Duff的设备是如何工作的。

更详细的解释是什么?

11 个答案:

答案 0 :(得分:216)

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

假设您正在复制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 :(得分:103)

explanation in Dr. Dobb's Journal是我在这个主题上发现的最好的。

这是我的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 :(得分:70)

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

第二个方面是switch语句。它允许代码第一次跳转到循环的中间。对大多数人来说,令人惊讶的部分是允许这样的事情。嗯,这是允许的。执行从计算的案例标签开始,然后通过到达每个连续的赋值语句,就像任何其他switch语句一样。在最后一个案例标签之后,执行到达循环的底部,此时它会跳回到顶部。循环的顶部是里面 switch语句,因此不再重新评估开关。

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

答案 3 :(得分:11)

duffs设备的目的是减少在紧密的memcpy实现中完成的比较次数。

假设您要将'count'字节从a复制到b,直接的方法是执行以下操作:

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

您需要多少次比较计数以查看它是否高于0? '数'次。

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

现在假设您想使用duffs设备复制20个字节,您需要进行多少次比较?只有3,因为你一次复制8个字节,除了 last 第一个只复制4个字节。

更新:您不必进行8次比较/ case-in-switch语句,但在功能大小和速度之间进行权衡取舍是合理的。

答案 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);
    }
}

我不知道发生了什么。

当问到这个问题时,可能不会,但现在Wikipedia has a very good explanation

  

凭借C中的两个属性

,设备有效,合法C.      
      
  • 在语言定义中放宽了switch语句的规范。在设备发明时,这是C编程语言的第一版,它只要求开关的受控语句是语法上有效的(复合)语句,在这种情况下,标签可以出现在任何子语句前面。结合以下事实:在没有break语句的情况下,控制流将从一个案例标签控制的语句转变为由下一个案例标签控制的语句,这意味着代码指定了一系列计数副本。顺序源地址到内存映射输出端口。
  •   
  • 在C中合法跳入循环中间的能力。
  •   

答案 5 :(得分: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情况并且循环代码运行'余数'次。

答案 6 :(得分: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次比较 - 这相当于次要的,但可能是重要的优化。

答案 7 :(得分:1)

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

“这是Duff's Device。这是展开循环的一种方法,避免了必须添加辅助修正循环来处理循环迭代次数不知道是循环次数的确切倍数的情况。展开因子。

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

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

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

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

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

答案 8 :(得分:0)

这里有一个非详细的解释,这就是我觉得Duff设备的关键所在:

问题是,C基本上是汇编语言的一个很好的外观(PDP-7程序集是具体的;如果你研究过,你会看到相似之处是多么惊人)。并且,在汇编语言中,您实际上没有循环 - 您有标签和条件分支指令。因此,循环只是整个指令序列的一部分,在某处有标签和分支:

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

并且开关指令在某种程度上分支/跳跃:

        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中的组合就不再那么奇怪了。

答案 9 :(得分: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;
}

答案 10 :(得分:0)

以下是使用Duff的设备进行64位memcpy的工作示例:

#include <iostream>
#include <memory>

inline void __memcpy(void* to, const void* from, size_t count)
{
    size_t numIter = (count  + 56) / 64;  // gives the number of iterations;  bit shift actually, not division
    size_t rest = count & 63; // % 64
    size_t rest7 = rest&7;
    rest -= rest7;

    // Duff's device with zero case handled:
    switch (rest) 
    {
        case 0:  if (count < 8)
                     break;
                 do { *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 56:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 48:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 40:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 32:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 24:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 16:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
        case 8:      *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
                } while (--numIter > 0);
    }

    switch (rest7)
    {
        case 7: *(((unsigned char*)to)+6) = *(((unsigned char*)from)+6);
        case 6: *(((unsigned short*)to)+2) = *(((unsigned short*)from)+2); goto case4;
        case 5: *(((unsigned char*)to)+4) = *(((unsigned char*)from)+4);
        case 4: case4: *((unsigned long*)to) = *((unsigned long*)from); break; 
        case 3: *(((unsigned char*)to)+2) = *(((unsigned char*)from)+2);
        case 2: *((unsigned short*)to) = *((unsigned short*)from); break;
        case 1: *((unsigned char*)to) = *((unsigned char*)from);
    }
}

void main()
{
    static const size_t NUM = 1024;

    std::unique_ptr<char[]> str1(new char[NUM+1]);  
    std::unique_ptr<char[]> str2(new char[NUM+1]);

    for (size_t i = 0 ; i < NUM ; ++ i)
    {
        size_t idx = (i % 62);
        if (idx < 26)
            str1[i] = 'a' + idx;
        else
            if (idx < 52)
                str1[i] = 'A' + idx - 26;
            else
                str1[i] = '0' + idx - 52;
    }

    for (size_t i = 0 ; i < NUM ; ++ i)
    {
        memset(str2.get(), ' ', NUM); 
        __memcpy(str2.get(), str1.get(), i);
        if (memcmp(str1.get(), str2.get(), i) || str2[i] != ' ')
        {
            std::cout << "Test failed for i=" << i;
        }

    }

    return;
}


它处理零长度的情况(在原始Duff的设备中假设num> 0)。 函数main()包含__memcpy的简单测试用例。