Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/c/57.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181

Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/visual-studio-2008/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C 达夫的设备是如何工作的?_C_Duffs Device - Fatal编程技术网

C 达夫的设备是如何工作的?

C 达夫的设备是如何工作的?,c,duffs-device,C,Duffs Device,我读过这本书,但我不明白。我真的很感兴趣,但我已经在那里读了好几遍解释,我仍然不明白达夫的设备是如何工作的 更详细的解释是什么?这是我在这个主题上找到的最好的解释 这是我的啊哈时刻: for (i = 0; i < len; ++i) { HAL_IO_PORT = *pSource++; } duffs设备的目的是减少在紧凑的memcpy实现中进行比较的次数 假设要将“count”字节从a复制到b,直接方法是执行以下操作: do {

我读过这本书,但我不明白。我真的很感兴趣,但我已经在那里读了好几遍解释,我仍然不明白达夫的设备是如何工作的

更详细的解释是什么?

这是我在这个主题上找到的最好的解释

这是我的啊哈时刻:

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

duffs设备的目的是减少在紧凑的memcpy实现中进行比较的次数

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

  do {                      
      *a = *b++;            
  } while (--count > 0);
您需要比较计数多少次才能确定它是否高于0?”数一数

现在,duff设备使用了一个令人讨厌的开关箱的意外副作用,它允许您减少计数/8所需的比较次数

现在假设您想使用duffs设备复制20个字节,需要多少比较?只有3个字节,因为一次只复制8个字节,而上一个字节只复制4个字节


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

虽然我不能100%确定您的要求,但下面是

达夫的设备地址的问题是一个循环展开的问题,毫无疑问,你会在你发布的Wiki链接上看到这一点。这基本上等同于优化运行时效率和内存占用。达夫的设备处理串行复制,而不仅仅是任何旧问题,但它是一个经典的例子,说明了如何通过减少循环中需要进行比较的次数来进行优化

作为另一个更容易理解的示例,假设您有一个要循环的项目数组,每次向其中添加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;
}

达夫的设备是用C语言实现这个想法的,但正如你在维基上看到的那样,它有一系列的副本。在上面的例子中,您看到的是10个对比,而在原稿中是100个——这相当于一个次要的优化,但可能是重要的优化。

达夫的设备有两个关键点。首先,我想这是最容易理解的部分,循环被展开了。通过避免检查循环是否完成以及跳回循环顶部所涉及的一些开销,这可以用更大的代码大小换取更高的速度。当CPU执行直线代码而不是跳跃时,它可以运行得更快

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


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

其他地方有一些很好的解释,但让我试试看。这在白板上容易多了!下面是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个字节


注:上面显示的原始达夫设备复制到to地址的I/O设备。因此,没有必要将指针*增加到。在两个内存缓冲区之间进行复制时,您需要使用*到++。

当我第一次读取它时,我将其自动格式化为此

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编程语言的第一版,它只要求开关的受控语句是语法上有效的com pound语句,其中大小写标签可以显示为任何子语句的前缀。在没有break语句的情况下,控制流将从一个case标签控制的语句流向下一个case标签控制的语句,这意味着代码指定了从顺序源地址到内存映射输出端口的连续计数副本。 在C中合法跳入循环中间的能力。
1:Duffs设备是循环展开的一种特殊实现。循环展开是一种优化技术,适用于在循环中执行N次的操作-您可以通过执行循环N/N次,然后在循环内联中展开循环代码N次,以程序大小换取速度,例如,替换:

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

如果N%N==0,这非常有效-不需要Duff!如果这不是真的,那么你必须处理剩余的问题——这是一种痛苦

2:Duffs设备与标准的循环展开有什么不同?
当N%N!=0整个do/while按照标准循环展开执行N/N次,因为情况0适用。在循环的最后一次第一次运行中,案例开始运行,我们运行循环代码“剩余”次数-剩余的“正常”运行循环。

以下是一个非详细的解释,我认为这是达夫设备的关键:

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

刚刚进行实验,发现另一种变体在不交错switch语句的情况下运行,并执行while循环:


从技术上讲,goto仍然实现了一个循环,但这个变体可能更具可读性。

这是我对另一个关于Duff的设备的问题的回答,这个问题在作为重复问题结束之前得到了一些升级。我认为它在这里提供了一点有价值的背景,说明了为什么应该避免这种构造

这是。这是一种展开循环的方法,避免了在循环迭代次数不知道是展开因子的精确倍数时,必须添加第二个修复循环

因为这里的大多数答案似乎都是积极的,所以我要强调它的缺点

有了这段代码,编译器将很难对循环体应用任何优化。如果您只是以简单循环的形式编写代码,那么现代编译器应该能够为您处理展开。通过这种方式,您可以保持可读性和性能,并希望对循环体应用其他优化

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

这种结果是盲目地手动优化您认为可能需要的任何代码的典型结果。它会阻止编译器正确地执行其工作,使代码可读性降低,更容易出现错误,并且通常会减慢速度。如果你一开始就用正确的方式做事,比如写简单的代码,然后分析瓶颈,然后进行优化,你永远不会想到使用这样的东西。无论如何,没有现代的CPU和编译器


理解它很好,但如果你真的使用过它,我会感到惊讶。

下面是一个使用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;
}


它处理原始达夫设备中长度为零的情况,假设num>0。
函数main包含_memcpy的简单测试用例。

注意,达夫的设备在switch语句中不限于8个重复。为什么不能使用-count,count=count-8来代替?然后用第二个循环来处理余数?哈菲兹,你可以用第二个循环来处理余数。但是现在你有两倍多的代码来完成同样的事情而不增加速度。剩余的4个字节是在循环的第一次迭代中复制的,而不是最后一次。不确定这是副作用,还是令人讨厌的,还是无意的。我想这只是因为你错过了关键部分。这不仅仅是循环展开。switch语句跳入循环的中间。这就是为什么这个设备看起来如此混乱。上面的循环总是执行10个副本的倍数,但达夫的循环执行任何数字。这是真的,但我试图简化描述
为这次行动做准备。也许我没有把它弄清楚!:好的帖子加上我必须从你那里找到一个好的答案来投票;2下,13下:。享受这个漂亮的回答徽章吧。这里最关键的事实是,由于C语言的一个怪癖,当它第一次到达while时,它会跳回并执行所有的语句,这让我很长时间都无法理解达夫的设备。因此,即使len%8是4,它也将执行案例4、案例2、案例2和案例1,然后跳回并从下一个循环开始执行所有案例。这是需要解释的部分,循环和switch语句的交互方式。Dobbs博士的文章很好,但是除了链接之外,答案没有添加任何内容。请参阅下面Rob Kennedy的答案,该答案实际上提供了一个重要的观点,即先处理剩余的传输大小,然后处理零个或多个8字节的传输块。在我看来,这是理解这段代码的关键。我是否遗漏了什么,或者在第二个代码段中,len%8字节将不会被复制?我被卡住了,忘记了如果不在案例语句列表的末尾编写break语句,C或任何其他语言将继续执行这些语句。所以,如果你想知道达夫的设备为什么能工作,这是它的一个关键部分。这个解释更有意义。我理解的关键是先复制余数,然后以8字节的块复制余数,这是不寻常的,因为正如大多数时候提到的,您将以8字节的块复制余数,然后复制余数。首先做剩余部分是理解此算法的关键。+1用于提及开关/while循环的疯狂放置/嵌套。不可能想象来自Java这样的语言…我对Duffs设备感兴趣,下面这个问题:所以我想我可以尝试澄清Duff-不确定它是否对现有答案有任何改进…如何跳过case 0:子句并继续检查do while循环(即跳过子句的参数?如果跳过do while循环之外的唯一子句,那么为什么开关不在那里结束?不要太仔细地看大括号。不要看电视,做那么多。相反,将switch和while视为老式的计算GOTO语句或带有偏移量的汇编程序jmp语句。开关进行一些计算,然后将jmps转换到正确的位置。while做了一个布尔检查,然后盲目地将jmps右移到do所在的位置。如果这很好,为什么每个人都不使用这个呢?有什么缺点吗?@AlphaGoku Readability.@AlphaGoku现代编译器能够利用类似的循环展开。终止条件在哪里?没有交错开关和循环,然后说开关循环:case 0:没有闪烁。这怎么不是交错的?@Sjoerd的意思是“循环的语言构造”,即。E交错开关和do{}while,这看起来很奇怪,可能很难理解。承认,措辞并不太精确,因为goto构造在技术上仍然实现了一个循环,只是为了找到一个更可读的变量。
        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...
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;
}
#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;
}