เหตุใด GCC จึงใช้การคูณด้วยจำนวนแปลก ๆ ในการใช้การหารจำนวนเต็ม


228

ฉันได้อ่านเกี่ยวกับการประกอบdivและการmulประกอบและฉันตัดสินใจที่จะเห็นพวกเขาในการดำเนินการโดยการเขียนโปรแกรมง่าย ๆ ใน C:

ไฟล์ Division.c

#include <stdlib.h>
#include <stdio.h>

int main()
{
    size_t i = 9;
    size_t j = i / 5;
    printf("%zu\n",j);
    return 0;
}

จากนั้นสร้างรหัสภาษาแอสเซมบลีด้วย:

gcc -S division.c -O0 -masm=intel

แต่การดูdivision.sไฟล์ที่สร้างขึ้นมันไม่มีการดำเนินการใด ๆ ของ div! แต่กลับกลายเป็นเวทมนตร์ดำบางชนิดที่มีการเปลี่ยนบิตและตัวเลขเวทย์มนตร์ นี่คือข้อมูลโค้ดที่คำนวณi/5:

mov     rax, QWORD PTR [rbp-16]   ; Move i (=9) to RAX
movabs  rdx, -3689348814741910323 ; Move some magic number to RDX (?)
mul     rdx                       ; Multiply 9 by magic number
mov     rax, rdx                  ; Take only the upper 64 bits of the result
shr     rax, 2                    ; Shift these bits 2 places to the right (?)
mov     QWORD PTR [rbp-8], rax    ; Magically, RAX contains 9/5=1 now, 
                                  ; so we can assign it to j

เกิดอะไรขึ้นที่นี่? เหตุใด GCC จึงไม่ใช้ div เลย มันสร้างหมายเลขเวทย์มนตร์นี้ได้อย่างไรและทำไมทุกอย่างทำงาน?


29
gcc เพิ่มประสิทธิภาพการแบ่งตามค่าคงที่ลองหารด้วย 2,3,4,5,6,7,8 และคุณจะเห็นรหัสที่แตกต่างกันมากสำหรับแต่ละกรณี
Jabberwocky

28
หมายเหตุ: หมายเลข Magic -3689348814741910323แปลงCCCCCCCCCCCCCCCDเป็นuint64_tหรือประมาณ (2 ^ 64) * 4/5
chux - Reinstate Monica

32
@qiubit: คอมไพเลอร์จะไม่สร้างรหัสที่ไม่มีประสิทธิภาพเพียงเพราะการปิดใช้งานการเพิ่มประสิทธิภาพ "การเพิ่มประสิทธิภาพ" เล็กน้อยที่ไม่เกี่ยวข้องกับการเรียงลำดับรหัสใหม่หรือการกำจัดตัวแปรจะดำเนินการโดยไม่คำนึงถึงตัวอย่าง คำสั่งแหล่งเดียวจะแปลรหัสที่มีประสิทธิภาพที่สุดสำหรับการดำเนินการแยก การปรับให้เหมาะสมของคอมไพเลอร์คำนึงถึงโค้ดที่อยู่โดยรอบแทนที่จะเป็นเพียงคำสั่งเดียว
Clifford

20
อ่านบทความที่ยอดเยี่ยมนี้: Labor of Division
Jester

9
คอมไพเลอร์บางตัวจะสร้างรหัสที่ไม่มีประสิทธิภาพอย่างผิดปกติเพราะการเพิ่มประสิทธิภาพถูกปิดใช้งาน โดยเฉพาะอย่างยิ่งพวกเขาจะทำเพื่อให้การดีบักง่ายเช่นความสามารถในการตั้งจุดพักในแต่ละบรรทัดของรหัส ในความเป็นจริงแล้ว GCC ค่อนข้างผิดปกติเพราะไม่มีโหมด "ไม่มีการปรับให้เหมาะสม" เนื่องจากการปรับให้เหมาะสมหลายอย่างนั้นเปิดใช้งานแล้ว นี่คือตัวอย่างที่คุณสามารถดูได้ด้วย GCC เสียงดังกราวในมืออื่น ๆ และ MSVC, จะออกมาเป็นคำแนะนำที่div -O0(cc @ clifford)
Cody Gray

คำตอบ:


169

การแบ่งจำนวนเต็มเป็นหนึ่งในการดำเนินการทางคณิตศาสตร์ที่ช้าที่สุดที่คุณสามารถดำเนินการกับโปรเซสเซอร์ที่ทันสมัยโดยมีความหน่วงแฝงสูงถึงหลายรอบ (สำหรับ x86 ดูตารางการสอนของ Agner Fog และคำแนะนำไมโครออร์แกน )

หากคุณรู้จักตัวหารล่วงหน้าคุณสามารถหลีกเลี่ยงการหารด้วยการแทนที่ด้วยชุดของการดำเนินการอื่น ๆ (การคูณการเพิ่มและการเลื่อน) ซึ่งมีผลเทียบเท่า แม้ว่าต้องมีการดำเนินการหลายอย่าง แต่ก็ยังคงเป็นเรื่องที่เร็วกว่าการหารด้วยจำนวนเต็ม

การใช้ตัวดำเนินการ C /ด้วยวิธีนี้แทนที่จะใช้ลำดับการเรียนการสอนที่เกี่ยวข้องdivเป็นเพียงวิธีการเริ่มต้นของ GCC ในการแบ่งค่าคงที่ มันไม่จำเป็นต้องปรับให้เหมาะสมระหว่างการดำเนินการและไม่เปลี่ยนแปลงอะไรแม้แต่การดีบัก (การใช้-Osสำหรับโค้ดขนาดเล็กจะทำให้ GCC ใช้งานdiv) การใช้ inverse แบบ multiplicative แทนการหารก็เหมือนกับการใช้leaแทนmulและadd

เป็นผลให้คุณมีแนวโน้มที่จะเห็นdivหรือidivในผลลัพธ์ถ้าตัวหารไม่เป็นที่รู้จักในเวลารวบรวม

สำหรับข้อมูลเกี่ยวกับวิธีการคอมไพเลอร์สร้างลำดับเหล่านี้เช่นเดียวกับรหัสที่จะช่วยให้คุณสามารถสร้างพวกเขาเอง (เกือบจะแน่นอนไม่จำเป็นถ้าคุณกำลังทำงานกับคอมไพเลอร์ braindead) ดูlibdivide


5
ฉันไม่แน่ใจว่ามันยุติธรรมที่จะรวมเข้าด้วยกันการดำเนินการ FP และจำนวนเต็มในการเปรียบเทียบความเร็ว @fuz บางที Sneftel ควรจะบอกว่าการแบ่งเป็นการดำเนินการจำนวนเต็มที่ช้าที่สุดที่คุณสามารถทำได้บนโปรเซสเซอร์ที่ทันสมัย? นอกจากนี้ยังมีลิงก์เชื่อมโยงไปยังคำอธิบายเพิ่มเติมของ "เวทย์มนตร์" นี้ในความคิดเห็น คุณคิดว่าพวกเขาเหมาะสมที่จะรวบรวมในคำตอบของคุณสำหรับการมองเห็น? 1 , 2 , 3
Cody Gray

1
เพราะลำดับของการดำเนินงานที่มีหน้าที่เหมือนกัน ...-O3นี้อยู่เสมอความต้องการแม้ใน คอมไพเลอร์ต้องสร้างรหัสที่ให้ผลลัพธ์ที่ถูกต้องสำหรับค่าอินพุตที่เป็นไปได้ทั้งหมด การเปลี่ยนแปลงนี้สำหรับจุดลอยตัว-ffast-mathเท่านั้นและ AFAIK จะไม่มีการเพิ่มประสิทธิภาพจำนวนเต็ม "อันตราย" (ด้วยการเปิดใช้งานการเพิ่มประสิทธิภาพคอมไพเลอร์อาจสามารถพิสูจน์บางสิ่งเกี่ยวกับช่วงของค่าที่เป็นไปได้ซึ่งช่วยให้สามารถใช้บางสิ่งที่ใช้ได้กับจำนวนเต็มที่มีเครื่องหมายที่ไม่เป็นลบเท่านั้น)
Peter Cordes

6
คำตอบที่แท้จริงคือการที่ -O0 GCC ยังคงเปลี่ยนรหัสผ่านการรับรองภายในเป็นส่วนหนึ่งของการเปลี่ยน C เป็นรหัสเครื่อง มันเพิ่งเกิดขึ้นที่ผกผันการคูณแบบแยกส่วนเปิดใช้งานตามค่าเริ่มต้นแม้จะอยู่ที่-O0(แต่ไม่ใช่ด้วย-Os) คอมไพเลอร์อื่น ๆ (เช่นเสียงดังกราว) จะใช้สำหรับ DIV คงไม่อำนาจของ -O02 ที่เกี่ยวข้อง: ผมคิดว่าผมรวมย่อหน้าเกี่ยวกับเรื่องนี้ในCollatz-คาดเดาคำตอบ asm ที่เขียนด้วยมือของฉัน
ปีเตอร์ Cordes

6
@PeterCordes และใช่ฉันคิดว่า GCC (และคอมไพเลอร์อื่น ๆ อีกมากมาย) ลืมเหตุผลที่ดีสำหรับ "สิ่งที่การเพิ่มประสิทธิภาพมีผลบังคับใช้เมื่อปิดใช้งานการเพิ่มประสิทธิภาพ" หลังจากใช้เวลาส่วนที่ดีกว่านี้ในหนึ่งวันเพื่อติดตามข้อผิดพลาด codegen ที่คลุมเครือฉันรู้สึกรำคาญเล็กน้อยในตอนนี้
Sneftel

9
@Sneftel: นั่นอาจเป็นเพราะจำนวนผู้พัฒนาแอปพลิเคชั่นที่บ่นกับนักพัฒนาคอมไพเลอร์เกี่ยวกับรหัสที่ทำงานเร็วกว่าที่คาดไว้นั้นค่อนข้างเล็ก
dan04

121

การหารด้วย 5 จะเหมือนกับการคูณ 1/5 ซึ่งก็เหมือนกับการคูณด้วย 4/5 และเลื่อนไปทางขวา 2 บิต ค่าที่เกี่ยวข้องอยู่CCCCCCCCCCCCCCCDในรูปของเลขฐานสิบหกซึ่งเป็นเลขฐานสองของ 4/5 ถ้าใส่หลังจุดเลขฐานสิบหก (เช่นไบนารีสำหรับสี่ในห้า0.110011001100เกิดซ้ำ - ดูด้านล่างเพื่อดูว่าทำไม) ฉันคิดว่าคุณสามารถรับมันได้จากที่นี่! คุณอาจต้องการตรวจสอบการคำนวณจุดคงที่ (แม้ว่าจะมีการปัดเศษเป็นจำนวนเต็มในตอนท้าย)

เหตุใดการคูณจะเร็วกว่าการหารและเมื่อตัวหารคงที่นี่จะเป็นเส้นทางที่เร็วกว่า

ดูReciprocal Multiplication ซึ่งเป็นบทช่วยสอนสำหรับการเขียนรายละเอียดเกี่ยวกับวิธีการทำงานอธิบายในแง่ของจุดคงที่ มันแสดงให้เห็นว่าอัลกอริทึมสำหรับการค้นหาการทำงานซึ่งกันและกันและวิธีการจัดการส่วนที่ลงนามและโมดูโล

ลองพิจารณาสักครู่ว่าทำไม0.CCCCCCCC...(hex) หรือ0.110011001100...เลขฐานสองคือ 4/5 หารเลขฐานสองแทนด้วย 4 (เลื่อนไปทางขวา 2 ตำแหน่ง) และเราจะได้รับ0.001100110011...โดยการตรวจสอบเล็กน้อยสามารถเพิ่มต้นฉบับเพื่อรับ0.111111111111...ซึ่งเห็นได้ชัดว่าเท่ากับ 1 วิธีเดียวกัน0.9999999...ในทศนิยมเท่ากับหนึ่ง ดังนั้นเราจึงรู้ว่าx + x/4 = 1ดังนั้น,5x/4 = 1 x=4/5นี่จะแสดงเป็นCCCCCCCCCCCCDเลขฐานสิบหกสำหรับการปัดเศษ (เป็นเลขฐานสองที่อยู่นอกเหนือจากการแสดงครั้งสุดท้ายจะเป็น1)


2
@ user2357112 โปรดโพสต์คำตอบของคุณเอง แต่ฉันไม่เห็นด้วย คุณสามารถคิดว่าการคูณเป็น 64.0 บิตโดย 0.64 บิตคูณให้คำตอบจุดคงที่ 128 บิตซึ่ง 64 บิตต่ำสุดจะถูกละทิ้งแล้วหารด้วย 4 (ตามที่ฉันชี้ให้เห็นในย่อหน้าแรก) คุณอาจจะสามารถหาคำตอบทางคณิตศาสตร์แบบแยกส่วนซึ่งอธิบายการเคลื่อนไหวของบิตได้ดีพอ ๆ กัน แต่ฉันค่อนข้างแน่ใจว่านี่เป็นคำอธิบาย
abligh

6
ค่านั้นจริง ๆ แล้ว "CCCCCCCCCCCCCCCD" D สุดท้ายมีความสำคัญทำให้แน่ใจว่าเมื่อผลลัพธ์ถูกตัดทอนส่วนที่แน่นอนออกมาพร้อมกับคำตอบที่ถูกต้อง
plugwash

4
ไม่เป็นไร. ฉันไม่เห็นว่าพวกเขารับ 64 บิตบนของผลการคูณ 128 บิต ไม่ใช่สิ่งที่คุณสามารถทำได้ในภาษาส่วนใหญ่ดังนั้นในตอนแรกฉันไม่ได้ตระหนักว่ามันกำลังเกิดขึ้น คำตอบนี้จะได้รับการปรับปรุงให้ดีขึ้นมากโดยการกล่าวถึงอย่างชัดเจนว่าการรับ 64 บิตบนของผลลัพธ์ 128- บิตนั้นเทียบเท่ากับการคูณด้วยจำนวนจุดคงที่และปัดเศษลง (นอกจากนี้ยังเป็นการดีที่จะอธิบายว่าทำไมต้องเป็น 4/5 แทนที่จะเป็น 1/5 และทำไมเราต้องปัดเศษ 4/5 ขึ้นไปแทนที่จะลง)
user2357112 รองรับ Monica

2
Afaict คุณจะต้องทราบว่าต้องมีข้อผิดพลาดมากเพียงใดในการแบ่งการหาร 5 ขึ้นไปข้ามขอบเขตการปัดเศษแล้วเปรียบเทียบกับข้อผิดพลาดกรณีที่เลวร้ายที่สุดในการคำนวณของคุณ สันนิษฐานว่านักพัฒนา gcc ได้ทำและสรุปว่าจะให้ผลลัพธ์ที่ถูกต้องเสมอ
plugwash

3
ที่จริงแล้วคุณอาจต้องตรวจสอบ 5 ค่าที่เป็นไปได้สูงสุดเท่านั้น
plugwash

60

โดยทั่วไปการคูณจะเร็วกว่าการหาร ดังนั้นหากเราสามารถหลีกเลี่ยงการคูณโดยส่วนกลับแทนเราสามารถเร่งการหารอย่างมีนัยสำคัญโดยค่าคงที่

รอยย่นคือเราไม่สามารถเป็นตัวแทนของกันและกันได้อย่างแน่นอน (เว้นเสียแต่ว่าการหารด้วยพลังของสอง แต่ในกรณีนั้นเรามักจะสามารถเปลี่ยนการหารเป็นการกะบิต) ดังนั้นเพื่อให้แน่ใจว่าคำตอบที่ถูกต้องเราต้องระวังว่าข้อผิดพลาดในส่วนกลับของเราไม่ทำให้เกิดข้อผิดพลาดในผลลัพธ์สุดท้ายของเรา

-3689348814741910323 คือ 0xCCCCCCCCCCCCCCCDCD ซึ่งเป็นค่าที่มากกว่า 4/5 ที่แสดงใน 0.64 fixed point

เมื่อเราคูณจำนวนเต็ม 64 บิตด้วยจำนวนจุดคงที่ 0.64 เราจะได้ผลลัพธ์ 64.64 เราตัดทอนค่าเป็นจำนวนเต็ม 64 บิต (ปัดเศษเป็นศูนย์ได้อย่างมีประสิทธิภาพ) จากนั้นทำการเลื่อนเพิ่มเติมซึ่งหารด้วยสี่และอีกครั้งโดยการตัดทอนอีกครั้งโดยดูที่ระดับบิตเป็นที่ชัดเจนว่าเราสามารถถือว่าการตัดทอนทั้งสอง

สิ่งนี้ทำให้เราเห็นอย่างชัดเจนว่าการหารด้วย 5 อย่างน้อย แต่มันให้คำตอบที่ถูกต้องกับเราหรือไม่?

เพื่อให้ได้คำตอบที่ถูกต้องข้อผิดพลาดจะต้องมีขนาดเล็กพอที่จะไม่ตอบคำถามข้ามขอบเขตการปัดเศษ

คำตอบที่ถูกต้องสำหรับการหารด้วย 5 จะมีส่วนที่เป็นเศษส่วนของ 0, 1/5, 2/5, 3/5 หรือ 4/5 ดังนั้นข้อผิดพลาดในเชิงบวกน้อยกว่า 1/5 ในผลลัพธ์ที่คูณและถูกเลื่อนจะไม่ส่งผลให้ข้ามขอบเขตการปัดเศษ

ข้อผิดพลาดในของเราคงเป็น (1/5) * 2 -64 ค่าของiน้อยกว่า 2 64ดังนั้นข้อผิดพลาดหลังจากการคูณจะน้อยกว่า 1/5 ส่วนหลังโดย 4 ข้อผิดพลาดน้อยกว่า (1/5) * 2 -2

(1/5) * 2 −2 <1/5 ดังนั้นคำตอบจะเท่ากับการแบ่งที่แน่นอนและปัดเศษเป็นศูนย์เสมอ


น่าเสียดายที่นี่ใช้ไม่ได้กับตัวหารทั้งหมด

ถ้าเราพยายามที่จะเป็นตัวแทน 4/7 เป็นจำนวน 0.64 จุดคงมีการปัดเศษห่างจากศูนย์เราจบลงด้วยข้อผิดพลาดของ (6/7) * 2 -64 หลังจากการคูณด้วยค่า i ที่ต่ำกว่า 2 64เราจะได้ข้อผิดพลาดภายใต้ 6/7 และหลังจากการหารด้วยสี่เราจะได้ข้อผิดพลาดภายใต้ 1.5 / 7 ซึ่งมากกว่า 1/7

ดังนั้นในการปรับใช้ดิวิชัน 7 ให้ถูกต้องเราต้องคูณด้วยจำนวนจุดคงที่ 0.65 เราสามารถนำไปใช้โดยการคูณด้วย 64 บิตที่ต่ำกว่าของจำนวนจุดคงที่ของเราจากนั้นเพิ่มหมายเลขเดิม (ซึ่งอาจล้นลงในบิตนำไป)


8
คำตอบนี้เปลี่ยนผู้เข้าร่วมการคูณแบบแยกส่วนจาก "คณิตศาสตร์ที่ดูซับซ้อนกว่าที่ฉันต้องการสละเวลา" เป็นสิ่งที่สมเหตุสมผล +1 สำหรับรุ่นที่เข้าใจง่าย ฉันไม่เคยต้องการทำอะไรนอกจากใช้ค่าคงที่ที่คอมไพเลอร์สร้างขึ้นดังนั้นฉันจึงอ่านบทความอื่น ๆ ที่อธิบายคณิตศาสตร์ได้อย่างเดียว
Peter Cordes

2
ฉันไม่เห็นอะไรเกี่ยวกับการคำนวณแบบแยกส่วนในรหัสเลย Dunno ซึ่งนักวิจารณ์คนอื่นได้รับมา
plugwash

3
มันเป็นแบบโมดูโล 2 ^ n เหมือนกับเลขจำนวนเต็มทั้งหมดในการลงทะเบียน en.wikipedia.org/wiki/…
Peter Cordes

4
@PeterCordes แปรผกผันการคูณแบบแยกส่วนจะใช้สำหรับส่วนที่แน่นอน AFAIK พวกเขาไม่ได้มีประโยชน์สำหรับการแบ่งทั่วไป
แฮโรลด์

4
@PeterCordes การคูณโดยการกำหนดจุดคงที่หรือไม่ ฉันไม่รู้ว่าสิ่งที่ทุกคนเรียกมันว่า แต่ฉันอาจจะเรียกมันว่ามันเป็นคำอธิบายที่ค่อนข้าง
แฮโรลด์

12

นี่คือลิงค์ไปยังเอกสารของอัลกอริทึมที่สร้างค่าและรหัสที่ฉันเห็นด้วย Visual Studio (ในกรณีส่วนใหญ่) และฉันถือว่ายังคงใช้ใน GCC สำหรับการหารของจำนวนเต็มตัวแปรโดยจำนวนคงที่

http://gmplib.org/~tege/divcnst-pldi94.pdf

ในบทความ uword มี N bits, udword มี 2N bits, n = ตัวเศษ = เงินปันผล, d = ตัวหาร = ตัวหาร, initially ถูกตั้งค่าเริ่มต้นเป็น ceil (log2 (d)), shpre ถูก pre-shift (ใช้ก่อนทวีคูณ ) = e = จำนวนของศูนย์ zero bits ใน d, shpost คือ post-shift (ใช้หลังจากคูณ), prec คือความแม่นยำ = N - e = N - shpre เป้าหมายคือเพิ่มประสิทธิภาพการคำนวณของ n / d โดยใช้ pre-shift, multiply และ post-shift

เลื่อนลงไปที่รูป 6.2 ซึ่งกำหนดวิธีสร้างตัวคูณ udword (ขนาดสูงสุดคือ N + 1 บิต) แต่ไม่อธิบายกระบวนการอย่างชัดเจน ฉันจะอธิบายด้านล่างนี้

รูปที่ 4.2 และรูปที่ 6.2 แสดงว่าตัวคูณสามารถลดลงเป็นตัวคูณ N หรือน้อยกว่าสำหรับตัวหารส่วนใหญ่ได้อย่างไร สมการ 4.5 อธิบายวิธีที่สูตรที่ใช้จัดการกับตัวคูณ N + 1 บิตในรูปที่ 4.1 และ 4.2

ในกรณีของตัวประมวลผล X86 ที่ทันสมัยและตัวประมวลผลอื่นการคูณเวลาได้รับการแก้ไขดังนั้น pre-shift ไม่ได้ช่วยตัวประมวลผลเหล่านี้ แต่ก็ยังช่วยลดตัวคูณจาก N + 1 บิตเป็นบิต N ฉันไม่รู้ว่า GCC หรือ Visual Studio กำจัด pre-shift สำหรับเป้าหมาย X86 หรือไม่

กลับไปที่รูปที่ 6.2 ตัวหาร (เงินปันผล) สำหรับ mlow และ mhigh สามารถมากกว่า udword เฉพาะเมื่อตัวหาร (ตัวหาร)> 2 ^ (N-1) (เมื่อℓ == N => mlow = 2 ^ (2N) ในกรณีนี้ การแทนที่ที่เหมาะสมที่สุดสำหรับ n / d เป็นการเปรียบเทียบ (ถ้า n> = d, q = 1, อื่น ๆ q = 0) ดังนั้นจึงไม่มีการสร้างตัวคูณ ค่าเริ่มต้นของ mlow และ mhigh จะเป็น N + 1 บิตและสามารถแบ่งใช้ udword / uword ได้สองตัวเพื่อสร้างค่า N + 1 บิตแต่ละตัว (mlow หรือ mhigh) การใช้ X86 ในโหมด 64 บิตเป็นตัวอย่าง:

; upper 8 bytes of dividend = 2^(ℓ) = (upper part of 2^(N+ℓ))
; lower 8 bytes of dividend for mlow  = 0
; lower 8 bytes of dividend for mhigh = 2^(N+ℓ-prec) = 2^(ℓ+shpre) = 2^(ℓ+e)
dividend  dq    2 dup(?)        ;16 byte dividend
divisor   dq    1 dup(?)        ; 8 byte divisor

; ...
        mov     rcx,divisor
        mov     rdx,0
        mov     rax,dividend+8     ;upper 8 bytes of dividend
        div     rcx                ;after div, rax == 1
        mov     rax,dividend       ;lower 8 bytes of dividend
        div     rcx
        mov     rdx,1              ;rdx:rax = N+1 bit value = 65 bit value

คุณสามารถทดสอบสิ่งนี้กับ GCC คุณได้เห็นวิธีการจัดการ j = i / 5 ดูวิธีจัดการ j = i / 7 (ซึ่งควรเป็นกรณีตัวคูณทวีคูณ N + 1 บิต)

ในโปรเซสเซอร์ปัจจุบันส่วนใหญ่ทวีคูณมีกำหนดเวลาที่แน่นอนดังนั้นจึงไม่จำเป็นต้องเปลี่ยนล่วงหน้า สำหรับ X86 ผลลัพธ์สุดท้ายคือชุดคำสั่งสองชุดสำหรับตัวหารส่วนใหญ่และชุดคำสั่งห้าชุดสำหรับตัวหารเช่น 7 (เพื่อจำลองตัวคูณ N + 1 บิตดังแสดงในสมการ 4.5 และรูปที่ 4.2 ของไฟล์ pdf) ตัวอย่างรหัส X86-64:

;       rax = dividend, rbx = 64 bit (or less) multiplier, rcx = post shift count
;       two instruction sequence for most divisors:

        mul     rbx                     ;rdx = upper 64 bits of product
        shr     rdx,cl                  ;rdx = quotient
;
;       five instruction sequence for divisors like 7
;       to emulate 65 bit multiplier (rbx = lower 64 bits of multiplier)

        mul     rbx                     ;rdx = upper 64 bits of product
        sub     rbx,rdx                 ;rbx -= rdx
        shr     rbx,1                   ;rbx >>= 1
        add     rdx,rbx                 ;rdx = upper 64 bits of corrected product
        shr     rdx,cl                  ;rdx = quotient
;       ...

บทความนั้นอธิบายการใช้งานใน gcc ดังนั้นฉันคิดว่ามันเป็นสมมติฐานที่ปลอดภัยที่ยังคงใช้อัลโกเดิมอยู่
Peter Cordes

บทความลงวันที่ 1994 อธิบายการใช้งานใน gcc ดังนั้นจึงมีเวลาสำหรับ gcc ในการอัปเดตอัลกอริทึมของมัน ในกรณีที่คนอื่นไม่มีเวลาตรวจสอบเพื่อดูว่า 94 ใน URL นั้นหมายถึงอะไร
Ed Grimm

0

ฉันจะตอบจากมุมที่แตกต่างกันเล็กน้อย: เพราะอนุญาตให้ทำ

C และ C ++ ถูกกำหนดกับเครื่องที่เป็นนามธรรม คอมไพเลอร์แปลงโปรแกรมนี้ในแง่ของเครื่องนามธรรมเป็นเครื่องคอนกรีตตามกฎราวกับว่า

  • คอมไพเลอร์ได้รับอนุญาตให้ทำการเปลี่ยนแปลงใด ๆ ตราบเท่าที่มันไม่เปลี่ยนพฤติกรรมที่สังเกตได้ตามที่ระบุโดยเครื่องนามธรรม ไม่มีความคาดหวังที่สมเหตุสมผลว่าคอมไพเลอร์จะเปลี่ยนรหัสของคุณอย่างตรงไปตรงมาที่สุด (แม้ในขณะที่โปรแกรมเมอร์ C จำนวนมากคิดอย่างนั้น) โดยปกติแล้วจะทำเช่นนี้เพราะคอมไพเลอร์ต้องการเพิ่มประสิทธิภาพการทำงานเมื่อเทียบกับวิธีการตรงไปตรงมา (ตามที่กล่าวไว้ในคำตอบอื่น ๆ ที่มีความยาว)
  • หากภายใต้สถานการณ์ใด ๆ คอมไพเลอร์ "เพิ่มประสิทธิภาพ" โปรแกรมที่ถูกต้องกับสิ่งที่มีพฤติกรรมที่สังเกตได้แตกต่างกันนั่นคือข้อผิดพลาดของคอมไพเลอร์
  • พฤติกรรมที่ไม่ได้กำหนดในรหัสของเรา (ล้นจำนวนเต็มลงนามเป็นตัวอย่างคลาสสิก) และสัญญานี้เป็นโมฆะ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.