ฉันได้อ่านบทความเกี่ยวกับ Wikipedia บนอุปกรณ์ของ Duffแล้วและฉันไม่เข้าใจ ฉันสนใจจริงๆ แต่ฉันได้อ่านคำอธิบายที่นั่นสองสามครั้งและฉันก็ยังไม่เข้าใจว่าอุปกรณ์ของ Duff ทำงานอย่างไร
คำอธิบายรายละเอียดเพิ่มเติมจะเป็นอย่างไร
ฉันได้อ่านบทความเกี่ยวกับ Wikipedia บนอุปกรณ์ของ Duffแล้วและฉันไม่เข้าใจ ฉันสนใจจริงๆ แต่ฉันได้อ่านคำอธิบายที่นั่นสองสามครั้งและฉันก็ยังไม่เข้าใจว่าอุปกรณ์ของ Duff ทำงานอย่างไร
คำอธิบายรายละเอียดเพิ่มเติมจะเป็นอย่างไร
คำตอบ:
มีคำอธิบายดีๆที่อื่น แต่ให้ฉันลองดู (นี่เป็นไวท์บอร์ดที่ง่ายกว่ามาก!) นี่คือตัวอย่างของ 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 ไบต์
หมายเหตุ: อุปกรณ์ของดัฟฟ์ดั้งเดิม (แสดงด้านบน) คัดลอกไปยังอุปกรณ์ I / O ตามที่to
อยู่ ดังนั้นจึงไม่จำเป็นที่จะต้องเพิ่มพ*to
อยน์เตอร์ *to++
เมื่อคัดลอกระหว่างสองหน่วยความจำบัฟเฟอร์ที่คุณจะต้องใช้
do
มาก ให้ดูที่switch
และที่while
เป็นแบบเก่าที่คำนวณGOTO
หรือjmp
งบแอสเซมเบลอร์ด้วยอ็อฟเซ็ต switch
ไม่คณิตศาสตร์บางส่วนแล้วjmp
s สถานที่ที่เหมาะสม while
ไม่เช็คบูลแล้วสุ่มสี่สุ่มห้าjmp
s ไปทางขวาเกี่ยวกับที่do
เป็น
คำอธิบายดร. 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++;
}
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);
}
len%8
เป็น 4 ก็จะดำเนินการกรณีที่ 4, กรณีที่ 2, กรณีที่ 2 และกรณีที่ 1 และจากนั้นกระโดดกลับและดำเนินการทุกกรณีจากการวนรอบต่อไปเป็นต้นไป นี่คือส่วนที่ต้องการอธิบายวิธีการวนรอบและคำสั่งสลับ "โต้ตอบ"
len % 8
จะไม่ถูกคัดลอก?
มีสองสิ่งสำคัญสำหรับอุปกรณ์ของ Duff ก่อนอื่นซึ่งฉันสงสัยว่าเป็นส่วนที่เข้าใจง่ายกว่าการวนซ้ำนั้นไม่ได้เลื่อน การค้าขนาดรหัสขนาดใหญ่กว่าสำหรับความเร็วเพิ่มเติมโดยการหลีกเลี่ยงค่าใช้จ่ายที่เกี่ยวข้องในการตรวจสอบว่าการวนซ้ำเสร็จแล้วและกระโดดกลับไปด้านบนของการวนรอบ ซีพียูสามารถทำงานได้เร็วขึ้นเมื่อรันโค้ดแบบเส้นตรงแทนที่จะกระโดด
กว้างยาวที่สองคือคำสั่งสลับ จะช่วยให้รหัสที่จะกระโดดลงในช่วงกลางของลูปเป็นครั้งแรกผ่าน ส่วนที่น่าประหลาดใจสำหรับคนส่วนใหญ่คือสิ่งที่ได้รับอนุญาต ได้รับอนุญาต การดำเนินการเริ่มต้นที่ป้ายชื่อเคสที่คำนวณได้และจากนั้นจะผ่านไปยังแต่ละคำสั่งการมอบหมายต่อเนื่องเช่นเดียวกับคำสั่งเปลี่ยนอื่น ๆ หลังจากเลเบลเคสสุดท้ายการดำเนินการมาถึงด้านล่างของลูปตรงจุดที่มันกระโดดกลับไปด้านบน ด้านบนของลูปอยู่ในคำสั่ง switch ดังนั้นสวิตช์จะไม่ถูกประเมินอีกครั้ง
การวนซ้ำดั้งเดิมนั้นไม่ได้วนรอบแปดครั้งดังนั้นจำนวนการวนซ้ำจะถูกหารด้วยแปด หากจำนวนไบต์ที่จะคัดลอกไม่ใช่หลายแปดก็มีบางส่วนที่เหลือ อัลกอริทึมส่วนใหญ่ที่คัดลอกบล็อกของไบต์ในแต่ละครั้งจะจัดการกับไบต์ที่เหลือในตอนท้าย แต่อุปกรณ์ของ Duff จัดการกับพวกเขาที่จุดเริ่มต้น ฟังก์ชั่นจะคำนวณcount % 8
หาคำสั่ง switch เพื่อคำนวณว่าส่วนที่เหลือจะเป็นเท่าไหร่ข้ามไปที่ป้ายชื่อเคสสำหรับหลายไบต์และคัดลอก จากนั้นลูปจะยังคงคัดลอกกลุ่มแปดไบต์
จุดของอุปกรณ์ดัฟฟ์คือการลดจำนวนการเปรียบเทียบในการใช้งาน memcpy ที่เข้มงวด
สมมติว่าคุณต้องการคัดลอก 'count' bytes จาก a ถึง b วิธีการส่งต่อโดยตรงคือการทำสิ่งต่อไปนี้:
do {
*a = *b++;
} while (--count > 0);
คุณต้องเปรียบเทียบจำนวนครั้งเพื่อดูว่าเป็น 0 ข้างต้นหรือไม่ 'นับ' ครั้ง
ตอนนี้อุปกรณ์ดัฟฟ์ใช้ผลข้างเคียงที่น่ารังเกียจโดยไม่ตั้งใจของตัวเรือนสวิตช์ซึ่งช่วยให้คุณลดจำนวนการเปรียบเทียบที่จำเป็นในการนับ / 8
ตอนนี้สมมติว่าคุณต้องการคัดลอก 20 ไบต์โดยใช้อุปกรณ์ดัฟฟ์คุณต้องการเปรียบเทียบกี่ชุด? เพียง 3 เนื่องจากคุณคัดลอกแปดไบต์ในเวลายกเว้นสุดท้ายคนแรกที่คุณคัดลอกเพียง 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 มีคำอธิบายที่ดีมาก
อุปกรณ์ถูกต้องตามกฎหมาย C โดยอาศัยสองคุณลักษณะใน C:
- ข้อมูลจำเพาะที่ผ่อนคลายของคำสั่งสวิตช์ในคำจำกัดความของภาษา ในช่วงเวลาของการประดิษฐ์อุปกรณ์นี้เป็นรุ่นแรกของภาษาการเขียนโปรแกรม C ซึ่งต้องการเพียงว่าคำสั่งควบคุมของสวิทช์เป็นคำสั่งที่ถูกต้อง syntactically (สารประกอบ) ภายในกรณีที่ป้ายชื่อสามารถปรากฏนำหน้าคำสั่งย่อยใด ๆ ร่วมกับความจริงที่ว่าหากไม่มีคำสั่ง break การไหลของการควบคุมจะลดลงจากคำสั่งที่ควบคุมโดยป้ายกำกับกรณีหนึ่งไปยังที่ควบคุมโดยถัดไปซึ่งหมายความว่ารหัสระบุการสืบทอดสำเนานับจาก แหล่งที่มาตามลำดับไปยังพอร์ตเอาต์พุตที่แม็พหน่วยความจำ
- ความสามารถในการกระโดดอย่างถูกกฎหมายในช่วงกลางของวงในซี
1: อุปกรณ์ดัฟฟ์เป็นสิ่งที่ทำให้เกิดการวนซ้ำของการคลายออก การเปิดลูปคืออะไร
หากคุณมีการดำเนินการเพื่อดำเนินการ 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 - ไม่จำเป็นต้องดัฟฟ์! หากไม่เป็นจริงคุณต้องจัดการส่วนที่เหลือ - ซึ่งเป็นความเจ็บปวด
2: อุปกรณ์ Duffs แตกต่างจากการวนลูปมาตรฐานนี้อย่างไร
อุปกรณ์ดัฟฟ์เป็นวิธีที่ชาญฉลาดในการจัดการกับวนรอบส่วนที่เหลือเมื่อ N% n! = 0 ทั้งทำ / ในขณะที่ดำเนินการจำนวน N / n จำนวนครั้งตามการเปิดลูปมาตรฐาน (เพราะกรณีที่ใช้ 0) ในการเรียกใช้ครั้งสุดท้ายผ่านลูป (เวลา 'N / n + 1'th) เคสเตะเข้าและเราข้ามไปยังเคส N% n และรันโค้ดลูปตามจำนวน' ส่วนที่เหลือ '
แม้ว่าฉันจะไม่แน่ใจ 100% ว่าคุณต้องการอะไร แต่ที่นี่จะไป ...
ปัญหาที่อยู่อุปกรณ์ของ Duff เป็นหนึ่งในวงคลี่คลาย (เนื่องจากคุณไม่ต้องสงสัยเลยว่าได้เห็นในลิงก์ Wiki ที่คุณโพสต์) สิ่งนี้โดยทั่วไปหมายถึงการเพิ่มประสิทธิภาพของเวลาทำงานมากกว่าการใช้หน่วยความจำ อุปกรณ์ของดัฟฟ์เกี่ยวข้องกับการทำสำเนาแบบอนุกรมมากกว่าปัญหาเก่า ๆ แต่เป็นตัวอย่างแบบคลาสสิกของวิธีการเพิ่มประสิทธิภาพที่สามารถทำได้โดยการลดจำนวนครั้งที่การเปรียบเทียบจะต้องทำในลูป
เป็นอีกทางเลือกหนึ่งซึ่งอาจทำให้เข้าใจง่ายขึ้นลองนึกภาพว่าคุณมีรายการที่คุณต้องการวนซ้ำและเพิ่ม 1 รายการในแต่ละครั้ง ... โดยปกติคุณอาจใช้วงรอบและวนรอบ 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 รายการซึ่งเป็นจำนวนที่น้อยกว่า แต่อาจมีการเพิ่มประสิทธิภาพที่สำคัญ
นี่คือคำอธิบายที่ไม่มีรายละเอียดซึ่งเป็นสิ่งที่ฉันรู้สึกว่าเป็นปมของอุปกรณ์ของ 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 จะดูไม่แปลกอีกต่อไป
นี่เป็นคำตอบที่ฉันโพสต์ไปยังคำถามอื่นเกี่ยวกับอุปกรณ์ของ Duff ที่มี upvaotes ก่อนที่คำถามจะปิดซ้ำ ฉันคิดว่ามันให้บริบทที่มีค่าเล็กน้อยที่นี่ทำไมคุณควรหลีกเลี่ยงการสร้างนี้
"นี่คืออุปกรณ์ของดัฟฟ์มันเป็นวิธีการวนลูปที่ไม่ต้องเพิ่มลูปการแก้ไขรองเพื่อจัดการกับเวลาที่จำนวนการวนซ้ำวนซ้ำไม่รู้ว่าเป็นปัจจัยทวีคูณแน่นอน
เนื่องจากคำตอบส่วนใหญ่ที่นี่ดูเหมือนจะเป็นบวกโดยทั่วไปเกี่ยวกับมันฉันจะเน้นข้อเสีย
ด้วยรหัสนี้คอมไพเลอร์จะพยายามใช้การปรับให้เหมาะสมกับร่างกายวน หากคุณเพิ่งเขียนโค้ดเป็นลูปธรรมดาคอมไพเลอร์สมัยใหม่ควรจะสามารถจัดการกับการคลี่คลายสำหรับคุณ วิธีนี้คุณจะรักษาความสามารถในการอ่านและประสิทธิภาพและมีความหวังในการเพิ่มประสิทธิภาพอื่น ๆ ที่นำไปใช้กับลูปเนื้อหา
บทความ Wikipedia ที่ผู้อื่นอ้างอิงอ้างถึงแม้เมื่อ 'รูปแบบ' นี้ถูกลบออกจากประสิทธิภาพของซอร์สโค้ด Xfree86 ก็ดีขึ้น
ผลลัพธ์นี้เป็นเรื่องปกติของการเพิ่มประสิทธิภาพของรหัสใด ๆ ที่คุณคิดว่าอาจจำเป็นต้องใช้ มันป้องกันคอมไพเลอร์จากการทำงานอย่างถูกต้องทำให้โค้ดของคุณอ่านน้อยลงและมีแนวโน้มที่จะเกิดข้อบกพร่องและมักจะช้าลง หากคุณกำลังทำสิ่งที่ถูกต้องตั้งแต่แรกคือการเขียนโค้ดอย่างง่ายจากนั้นทำโปรไฟล์หาคอขวดจากนั้นปรับให้เหมาะสมคุณจะไม่เคยคิดที่จะใช้สิ่งนี้ ไม่ได้มีซีพียูและคอมไพเลอร์รุ่นใหม่อยู่ดี
ไม่เป็นไรที่จะเข้าใจ แต่ฉันจะแปลกใจถ้าคุณเคยใช้มันจริง ๆ "
เพียงแค่การทดสอบพบว่ามีตัวแปรอื่นเข้ามาโดยไม่ต้องใช้สวิตช์และลูป
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;
}