达夫的设备如何工作?

我已经阅读了达夫设备上的维基百科文章 ,但我不明白。 我真的很感兴趣,但是我已经阅读了几次这个解释,但是我仍然不明白达夫的设备是如何工作的。

更详细的解释是什么?

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

假设您正在复制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设备(如上所示)复制到地址的I / O设备。 因此,没有必要增加指针*to 。 在两个内存缓冲区之间进行复制时,您需要使用*to++

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++; } 

:)变成:

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

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

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

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

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

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

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

你需要多少次比较计数,看它是否大于0? “数”次。

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

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

更新:你不必做8个比较/ case-in-switch语句,但是在函数大小和速度之间进行折衷是合理的。

当我第一次阅读它的时候,我自动把它写成这个

 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语句的规范。 在设备发明的时候,这是“C语言程序设计语言”的第一版,它只要求开关的受控语句是一个语法上有效的(复合)语句,在这种语句中,标签可以出现在任何子语句的前面。 结合这样一个事实,如果没有中断声明,控制流程将从一个案例标签控制的声明到下一个控制的声明中,这意味着代码指定了一系列从顺序的源地址到存储器映射的输出端口。
  • 合法跳到C中循环的能力

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的情况,并运行循环代码“余数”的次数。

虽然我不是100%确定你在问什么,这里是…

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

作为一个替代的例子,这可能会让你更容易理解,假设你有一个你想要循环的项目数组,并且每次给它们加1 …通常,你可以使用for循环,循环100次。 这似乎是相当合理的,这是…但是,优化可以通过展开循环(显然不太远…或者你也可以不使用循环)。

所以一个常规的循环:

 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; } 

达夫的设备所做的是用C实现这个想法,但是(如你在维基上看到的)用串行拷贝来实现。 上面的例子显示了10个比较,而原来的比较是100个 – 这相当于一个小的但可能很重要的优化。

这里是一个非详细的解释,这是我觉得是达夫的设备的关键:

问题是,对于汇编语言来说,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中的组合似乎不再那么怪异。

只是尝试,发现另一个变种相处,而不交错开关和循环:

 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; }