Tôi đã đọc bài viết trên Wikipedia trên thiết bị của Duff và tôi không hiểu. Tôi thực sự thích thú, nhưng tôi đã đọc lời giải thích ở đó một vài lần và tôi vẫn không hiểu thiết bị của Duff hoạt động như thế nào.
Điều gì sẽ giải thích chi tiết hơn?
Tôi đã đọc bài viết trên Wikipedia trên thiết bị của Duff và tôi không hiểu. Tôi thực sự thích thú, nhưng tôi đã đọc lời giải thích ở đó một vài lần và tôi vẫn không hiểu thiết bị của Duff hoạt động như thế nào.
Điều gì sẽ giải thích chi tiết hơn?
Câu trả lời:
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++
.
do
quá nhiều. Thay vào đó, hãy nhìn vào switch
và các câu lệnh được while
tính toán theo kiểu cũ GOTO
hoặc các jmp
câu lệnh trình biên dịch với phần bù. Làm switch
một số toán học và sau đó jmp
s đến đúng nơi. Việc while
kiểm tra boolean và sau đó mù quáng jmp
sang phải về nơi do
đã được.
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);
}
len%8
là 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".
len % 8
byte sẽ không được sao chép?
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 % 8
cho 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.
Đ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 độ.
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.
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'.
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.
Đâ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.
Đâ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ó. "
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;
}