Thiết bị của Duff hoạt động như thế nào?


Câu trả lời:


240

Có một số giải thích tốt ở nơi khác, nhưng hãy để tôi thử. (Điều này dễ hơn rất nhiều trên bảng trắng!) Đây là ví dụ Wikipedia với một số ký hiệu.

Giả sử bạn đang sao chép 20 byte. Điều khiển luồng của chương trình cho lần đầu tiên là:

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

Bây giờ, bắt đầu vượt qua thứ hai, chúng tôi chỉ chạy mã được chỉ định:

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

Bây giờ, bắt đầu vượt qua thứ ba:

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 byte hiện được sao chép.

Lưu ý: Thiết bị gốc của Duff (hiển thị ở trên) được sao chép sang thiết bị I / O tại tođịa chỉ. Vì vậy, không cần thiết phải tăng con trỏ *to. Khi sao chép giữa hai bộ nhớ đệm bạn cần sử dụng *to++.


1
Làm thế nào có thể bỏ qua mệnh đề 0: và tiếp tục kiểm tra các mệnh đề khác nằm trong vòng lặp do while là đối số của mệnh đề bị bỏ qua? Nếu mệnh đề duy nhất nằm ngoài vòng lặp do while bị bỏ qua, tại sao công tắc không kết thúc ở đó?
Aurelius

14
Đừng nhìn niềng răng quá khó. Đừng nhìn vào doquá nhiều. Thay vào đó, hãy nhìn vào switchvà các câu lệnh được whiletính toán theo kiểu cũ GOTOhoặc các jmpcâu lệnh trình biên dịch với phần bù. Làm switchmột số toán học và sau đó jmps đến đúng nơi. Việc whilekiểm tra boolean và sau đó mù quáng jmpsang phải về nơi dođã được.
Clinton Pierce

Nếu điều này là tốt, tại sao mọi người không sử dụng nó? Có bất kỳ nhược điểm?
AlphaGoku

@AlphaGoku Dễ đọc.
LF

108

Giải thích trên Tạp chí của Tiến sĩ Dobb là tốt nhất mà tôi tìm thấy về chủ đề này.

Đây là khoảnh khắc AHA của tôi:

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

trở thành:

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

trở thành:

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

bài đăng tốt (cộng với tôi phải tìm một câu trả lời hay từ bạn để upvote;) 2 xuống, 13 để đi: stackoverflow.com/questions/359727#486543 ). Thưởng thức huy hiệu câu trả lời tốt đẹp.
VonC

13
Sự thật quan trọng ở đây, và khiến cho thiết bị của Duff không thể hiểu được tôi trong thời gian dài nhất, là bởi một sự châm biếm của C, sau lần đầu tiên nó đạt được, nó nhảy trở lại và thực hiện tất cả các tuyên bố. Do đó, ngay cả khi len%8là 4, nó sẽ thực hiện trường hợp 4, trường hợp 2, trường hợp 2 và trường hợp 1, sau đó nhảy trở lại và thực hiện tất cả các trường hợp từ vòng lặp tiếp theo trở đi. Đây là phần cần giải thích, cách vòng lặp và câu lệnh chuyển đổi "tương tác".
ShreevatsaR

2
Bài viết của Tiến sĩ Dobbs là tốt tuy nhiên ngoài liên kết, câu trả lời không thêm gì cả. Xem câu trả lời của Rob Kennedy dưới đây thực sự cung cấp một điểm quan trọng về phần còn lại của kích thước chuyển được xử lý trước tiên bằng 0 hoặc nhiều khối chuyển 8 byte. Theo tôi đó là chìa khóa để hiểu mã này.
Richard Chambers

3
Tôi có thiếu thứ gì không, hoặc trong đoạn mã đoạn mã thứ hai len % 8byte sẽ không được sao chép?
newbie

Tôi đã bị mắc kẹt, quên rằng nếu bạn không viết một tuyên bố phá vỡ ở cuối danh sách tuyên bố của một trường hợp, C (hoặc bất kỳ ngôn ngữ nào khác) sẽ tiếp tục thực hiện các tuyên bố. Vì vậy, nếu bạn đang tự hỏi tại sao thiết bị của duff hoạt động hoàn toàn, thì đây là một phần quan trọng của thiết bị
goonerify

75

Có hai điều quan trọng đối với thiết bị của Duff. Đầu tiên, mà tôi nghi ngờ là phần dễ hiểu hơn, vòng lặp không được kiểm soát. Điều này giao dịch kích thước mã lớn hơn để có tốc độ cao hơn bằng cách tránh một số chi phí liên quan đến việc kiểm tra xem vòng lặp đã kết thúc hay chưa và quay trở lại đỉnh của vòng lặp. CPU có thể chạy nhanh hơn khi thực thi mã đường thẳng thay vì nhảy.

Khía cạnh thứ hai là câu lệnh switch. Nó cho phép mã nhảy vào giữa vòng lặp lần đầu tiên thông qua. Điều đáng ngạc nhiên với hầu hết mọi người là một điều như vậy được cho phép. Vâng, nó được cho phép. Thực thi bắt đầu tại nhãn trường hợp được tính toán, và sau đó nó rơi vào từng câu lệnh gán liên tiếp, giống như bất kỳ câu lệnh chuyển đổi nào khác. Sau nhãn trường hợp cuối cùng, thực thi đạt đến đáy của vòng lặp, tại đó nó nhảy trở lại đầu trang. Đỉnh của vòng lặp nằm trong câu lệnh switch, vì vậy khóa chuyển không được đánh giá lại nữa.

Vòng lặp ban đầu là tám lần, vì vậy số lần lặp được chia cho tám. Nếu số byte được sao chép không phải là bội số của tám, thì có một số byte còn lại. Hầu hết các thuật toán sao chép các khối byte tại một thời điểm sẽ xử lý các byte còn lại ở cuối, nhưng thiết bị của Duff xử lý chúng ở đầu. Hàm tính toán count % 8cho câu lệnh chuyển đổi để tìm ra phần còn lại sẽ là gì, nhảy đến nhãn trường hợp cho nhiều byte đó và sao chép chúng. Sau đó, vòng lặp tiếp tục sao chép các nhóm tám byte.


5
Giải thích này có ý nghĩa hơn. Chìa khóa để tôi hiểu rằng phần còn lại được sao chép trước sau đó phần còn lại trong các khối 8 byte, điều này không bình thường vì hầu hết thời gian, bạn sẽ sao chép trong các khối 8 byte và sau đó sao chép phần còn lại. làm phần còn lại trước tiên là chìa khóa để hiểu thuật toán này.
Richard Chambers

+1 để đề cập đến vị trí điên / lồng của vòng lặp switch / while. Không thể tưởng tượng được đến từ một ngôn ngữ như Java ...
Parobay

13

Điểm của thiết bị duffs là giảm số lượng so sánh được thực hiện trong một triển khai memcpy chặt chẽ.

Giả sử bạn muốn sao chép 'đếm' byte từ a sang b, cách tiếp cận thẳng là thực hiện như sau:

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

Bạn cần so sánh số lần bao nhiêu lần để xem nó có phải là 0 không? 'đếm' lần.

Bây giờ, thiết bị duff sử dụng hiệu ứng phụ không chủ ý khó chịu của vỏ công tắc cho phép bạn giảm số lượng so sánh cần thiết để đếm / 8.

Bây giờ giả sử bạn muốn sao chép 20 byte bằng thiết bị duffs, bạn cần bao nhiêu so sánh? Chỉ có 3, vì bạn sao chép tám byte mỗi lần trừ cái đầu tiên cuối cùng trong đó bạn sao chép chỉ 4.

CẬP NHẬT: Bạn không phải thực hiện 8 câu so sánh / trường hợp chuyển đổi, nhưng đó là sự đánh đổi hợp lý giữa kích thước chức năng và tốc độ.


3
Lưu ý rằng thiết bị của duff không bị giới hạn ở 8 lần trùng lặp trong câu lệnh chuyển đổi.
strager

tại sao bạn không thể sử dụng thay vì - đếm, đếm = đếm-8? và sử dụng một vòng lặp thứ hai để đối phó với phần còn lại?
hhafez

1
Hhafez, bạn có thể sử dụng vòng lặp thứ hai để đối phó với phần còn lại. Nhưng bây giờ bạn đã tăng gấp đôi số mã để thực hiện điều tương tự mà không tăng tốc độ.
Rob Kennedy

Johan, bạn có nó lạc hậu. 4 byte còn lại được sao chép trên lần lặp đầu tiên của vòng lặp, không phải lần cuối cùng.
Rob Kennedy

8

Khi tôi đọc nó lần đầu tiên, tôi đã tự động định dạng nó thành cái này

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

và tôi không biết chuyện gì đang xảy ra.

Có thể không phải khi câu hỏi này được hỏi, nhưng bây giờ Wikipedia có một lời giải thích rất tốt

Thiết bị hợp lệ, hợp pháp C nhờ hai thuộc tính trong C:

  • Đặc tả thư giãn của câu lệnh chuyển đổi trong định nghĩa của ngôn ngữ. Tại thời điểm phát minh ra thiết bị, đây là phiên bản đầu tiên của Ngôn ngữ lập trình C, chỉ yêu cầu câu lệnh được điều khiển của công tắc là câu lệnh (hợp chất) hợp lệ trong đó nhãn trường hợp có thể xuất hiện tiền tố cho bất kỳ câu lệnh phụ nào. Cùng với thực tế là, trong trường hợp không có tuyên bố phá vỡ, luồng kiểm soát sẽ chuyển từ một tuyên bố được kiểm soát bởi một nhãn trường hợp sang điều khiển tiếp theo, điều này có nghĩa là mã xác định kế tiếp các bản sao đếm từ địa chỉ nguồn tuần tự đến cổng đầu ra được ánh xạ bộ nhớ.
  • Khả năng nhảy hợp pháp vào giữa một vòng lặp trong C.

6

1: Thiết bị Duffs là một ý nghĩa đặc biệt của việc không kiểm soát vòng lặp. Unrolling vòng lặp là gì?
Nếu bạn có một thao tác để thực hiện N lần trong một vòng lặp, bạn có thể giao dịch kích thước chương trình để lấy tốc độ bằng cách thực hiện vòng lặp N / n lần và sau đó trong vòng lặp nội tuyến (không kiểm soát) mã vòng lặp n lần, ví dụ: thay thế:

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

với

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

Sẽ rất tuyệt nếu N% n == 0 - không cần Duff! Nếu điều đó không đúng thì bạn phải xử lý phần còn lại - đó là một nỗi đau.

2: Thiết bị Duffs khác với việc hủy đăng ký vòng lặp tiêu chuẩn này như thế nào?
Thiết bị Duffs chỉ là một cách xử lý thông minh với các chu kỳ vòng lặp còn lại khi N% n! = 0. Toàn bộ số lần thực hiện / trong khi thực hiện số lần N / n theo vòng lặp tiêu chuẩn (vì trường hợp 0 ​​được áp dụng). Trong lần chạy cuối cùng qua vòng lặp (lần 'N / n + 1'), trường hợp bắt đầu và chúng tôi nhảy đến trường hợp N% n và chạy mã vòng lặp số lần 'còn lại'.


Tôi có hứng thú với thiết bị Duffs sau câu hỏi này: stackoverflow.com/questions/17192246/switch-case-weird-scoping vì vậy Id nghĩ rằng sẽ làm rõ Duff - không chắc liệu nó có cải thiện gì về câu trả lời hiện tại không ...
Ricibob

3

Mặc dù tôi không chắc chắn 100% những gì bạn yêu cầu, nhưng ở đây ...

Vấn đề mà địa chỉ thiết bị của Duff là một trong những vấn đề lặp lại (như bạn chắc chắn đã thấy trên liên kết Wiki bạn đã đăng). Điều này về cơ bản tương đương với việc tối ưu hóa hiệu quả thời gian chạy, qua dấu chân bộ nhớ. Thiết bị của Duff xử lý sao chép nối tiếp, thay vì chỉ là bất kỳ vấn đề cũ nào, nhưng là một ví dụ kinh điển về cách tối ưu hóa có thể được thực hiện bằng cách giảm số lần so sánh cần thực hiện trong một vòng lặp.

Một ví dụ khác, có thể giúp bạn dễ hiểu hơn, hãy tưởng tượng bạn có một mảng các mặt hàng bạn muốn lặp lại và thêm 1 cho chúng mỗi lần ... thông thường, bạn có thể sử dụng vòng lặp for và lặp khoảng 100 lần . Điều này có vẻ khá logic và, tuy nhiên, ... tuy nhiên, việc tối ưu hóa có thể được thực hiện bằng cách tháo vòng lặp (rõ ràng không quá xa ... hoặc bạn cũng có thể không sử dụng vòng lặp).

Vì vậy, một vòng lặp thường xuyên:

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

trở thành

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

Những gì thiết bị của Duff thực hiện là thực hiện ý tưởng này, bằng C, nhưng (như bạn đã thấy trên Wiki) với các bản sao nối tiếp. Những gì bạn đang thấy ở trên, với ví dụ không rõ ràng, là 10 so sánh so với 100 trong bản gốc - điều này tương đương với một sự tối ưu nhỏ, nhưng có thể là quan trọng.


8
Bạn đang thiếu phần quan trọng. Nó không chỉ là về việc tháo gỡ vòng lặp. Câu lệnh switch nhảy vào giữa vòng lặp. Đó là những gì làm cho thiết bị trông rất khó hiểu. Vòng lặp của bạn ở trên luôn thực hiện bội số của 10 bản sao, nhưng Duff thực hiện bất kỳ số nào.
Rob Kennedy

2
Điều đó đúng - nhưng tôi đã cố gắng đơn giản hóa mô tả cho OP. Có lẽ tôi đã không làm rõ điều đó đủ! :)
James B

2

Đây là một lời giải thích không chi tiết, đó là điều tôi cảm thấy là mấu chốt của thiết bị của Duff:

Vấn đề là, C về cơ bản là một mặt tiền đẹp cho ngôn ngữ lắp ráp (lắp ráp PDP-7 cụ thể; nếu bạn nghiên cứu rằng bạn sẽ thấy sự tương đồng nổi bật như thế nào). Và, trong ngôn ngữ lắp ráp, bạn không thực sự có vòng lặp - bạn có nhãn và hướng dẫn chi nhánh có điều kiện. Vì vậy, vòng lặp chỉ là một phần của chuỗi hướng dẫn tổng thể có nhãn và nhánh ở đâu đó:

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

và một hướng dẫn chuyển đổi là phân nhánh / nhảy về phía trước một chút:

        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...

Khi lắp ráp, có thể dễ dàng hiểu được cách kết hợp hai cấu trúc điều khiển này và khi bạn nghĩ về nó theo cách đó, sự kết hợp của chúng trong C dường như không còn quá kỳ lạ nữa.


1

Đây là câu trả lời tôi đã đăng cho một câu hỏi khác về Thiết bị của Duff có một số upvaote trước khi câu hỏi được đóng lại dưới dạng trùng lặp. Tôi nghĩ rằng nó cung cấp một chút bối cảnh có giá trị ở đây về lý do tại sao bạn nên tránh cấu trúc này.

"Đây là thiết bị của Duff . Đây là một phương pháp không kiểm soát các vòng lặp để tránh phải thêm một vòng lặp sửa lỗi thứ cấp để xử lý thời gian khi số lần lặp lặp không biết là bội số chính xác của hệ số không kiểm soát.

Vì hầu hết các câu trả lời ở đây dường như nói chung là tích cực về nó, tôi sẽ làm nổi bật những nhược điểm.

Với mã này, một trình biên dịch sẽ phải vật lộn để áp dụng bất kỳ tối ưu hóa nào cho thân vòng lặp. Nếu bạn chỉ viết mã dưới dạng một vòng lặp đơn giản, trình biên dịch hiện đại sẽ có thể xử lý việc hủy đăng ký cho bạn. Bằng cách này bạn duy trì khả năng đọc và hiệu suất và có một số hy vọng về các tối ưu hóa khác được áp dụng cho thân vòng lặp.

Bài viết Wikipedia được những người khác tham khảo thậm chí còn nói khi 'mẫu' này bị xóa khỏi hiệu suất mã nguồn Xfree86 thực sự được cải thiện.

Kết quả này là điển hình của việc tối ưu hóa bàn tay một cách mù quáng bất kỳ mã nào bạn nghĩ có thể cần nó. Nó ngăn trình biên dịch thực hiện đúng công việc của nó, làm cho mã của bạn ít đọc hơn và dễ bị lỗi hơn và thường làm chậm nó. Nếu bạn đang làm mọi thứ đúng cách ngay từ đầu, tức là viết mã đơn giản, sau đó lập hồ sơ cho các nút thắt cổ chai, sau đó tối ưu hóa, bạn thậm chí sẽ không bao giờ nghĩ sẽ sử dụng một cái gì đó như thế này. Không phải với một CPU và trình biên dịch hiện đại nào.

Thật tốt khi hiểu nó, nhưng tôi sẽ ngạc nhiên nếu bạn thực sự sử dụng nó. "


0

Chỉ cần thử nghiệm, đã tìm thấy một biến thể khác hòa hợp với nhau mà không xen kẽ công tắc và vòng lặp:

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

Điều kiện chấm dứt của bạn ở đâu?
dùng2338150
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.