เหตุใดคำแนะนำ x86-64 บน 32 บิตจึงลงทะเบียนส่วนบนของการลงทะเบียน 64 บิตแบบเต็มเป็นศูนย์


119

ในx86-64 Tour of Intel Manualsฉันอ่าน

บางทีความจริงที่น่าประหลาดใจที่สุดก็คือคำสั่งเช่นMOV EAX, EBXการRAXลงทะเบียน32 บิตบนเป็นศูนย์โดยอัตโนมัติ

เอกสารของ Intel (3.4.1.1 General-Purpose Registers ในโหมด 64 บิตในสถาปัตยกรรมพื้นฐานแบบแมนนวล) ที่อ้างถึงในแหล่งข้อมูลเดียวกันบอกเราว่า:

  • ตัวถูกดำเนินการ 64 บิตสร้างผลลัพธ์ 64 บิตในการลงทะเบียนวัตถุประสงค์ทั่วไปปลายทาง
  • ตัวถูกดำเนินการ 32 บิตสร้างผลลัพธ์ 32 บิตขยายศูนย์เป็นผลลัพธ์ 64 บิตในการลงทะเบียนวัตถุประสงค์ทั่วไปปลายทาง
  • ตัวถูกดำเนินการ 8 บิตและ 16 บิตสร้างผลลัพธ์ 8 บิตหรือ 16 บิต 56 บิตด้านบนหรือ 48 บิต (ตามลำดับ) ของรีจิสเตอร์วัตถุประสงค์ทั่วไปปลายทางจะไม่ถูกแก้ไขโดยการดำเนินการ หากผลลัพธ์ของการดำเนินการ 8 บิตหรือ 16 บิตมีไว้สำหรับการคำนวณแอดเดรส 64 บิตให้ลงชื่อ - ขยายรีจิสเตอร์เป็น 64 บิตเต็ม

ในการประกอบ x86-32 และ x86-64 คำแนะนำ 16 บิตเช่น

mov ax, bx

อย่าแสดงพฤติกรรม "แปลก ๆ " แบบนี้ว่าคำบนของ eax เป็นศูนย์

ดังนั้น: อะไรคือสาเหตุที่ทำให้เกิดพฤติกรรมนี้? เมื่อมองแวบแรกดูเหมือนจะไร้เหตุผล (แต่เหตุผลอาจเป็นเพราะฉันคุ้นเคยกับการประกอบ x86-32)


16
หากคุณใช้ Google สำหรับ "แผงขายทะเบียนบางส่วน" คุณจะพบข้อมูลเล็กน้อยเกี่ยวกับปัญหาที่พวกเขาพยายามหลีกเลี่ยง (เกือบจะแน่นอน)
Jerry Coffin


4
ไม่ใช่แค่ "ส่วนใหญ่". AFAIK คำแนะนำทั้งหมดที่มีr32ตัวถูกดำเนินการปลายทางเป็นศูนย์ที่ 32 สูงแทนที่จะรวม ตัวอย่างเช่นแอสเซมเบลอร์บางตัวจะแทนที่pmovmskb r64, xmmด้วยpmovmskb r32, xmmการบันทึก REX เนื่องจากเวอร์ชันปลายทาง 64 บิตทำงานเหมือนกัน แม้ว่าส่วนการทำงานของคู่มือจะแสดงรายการชุดค่าผสมทั้งหมด 6 ชุดของแหล่งที่มา 32/64 บิตและ 64/128 / 256b แยกกัน แต่ส่วนขยายศูนย์โดยนัยของแบบฟอร์ม r32 จะซ้ำกับส่วนขยายศูนย์ที่ชัดเจนของฟอร์ม r64 ฉันอยากรู้เกี่ยวกับการใช้งาน HW ...
Peter Cordes

2
@HansPassant การอ้างอิงแบบวงกลมเริ่มต้นขึ้น
kchoi

คำตอบ:


98

ฉันไม่ใช่ AMD หรือพูดเพื่อพวกเขา แต่ฉันจะทำแบบเดียวกัน เนื่องจากการตั้งศูนย์ครึ่งสูงไม่ได้สร้างการพึ่งพาค่าก่อนหน้านี้ซีพียูจึงต้องรอ ทะเบียนเปลี่ยนชื่อกลไกหลักจะจะพ่ายแพ้ถ้ามันไม่ได้ทำวิธีการที่

ด้วยวิธีนี้คุณสามารถเขียนโค้ดอย่างรวดเร็วโดยใช้ค่า 32 บิตในโหมด 64 บิตโดยไม่ต้องทำลายการอ้างอิงอย่างชัดเจนตลอดเวลา หากไม่มีพฤติกรรมนี้คำสั่ง 32 บิตทุกคำสั่งในโหมด 64 บิตจะต้องรอสิ่งที่เกิดขึ้นก่อนแม้ว่าส่วนสูงนั้นแทบจะไม่เคยถูกใช้ก็ตาม (การทำint64 บิตจะเป็นการสิ้นเปลืองพื้นที่แคชและแบนด์วิดท์หน่วยความจำx86-64 รองรับขนาดตัวถูกดำเนินการ 32 และ 64 บิตอย่างมีประสิทธิภาพมากที่สุด )

ลักษณะการทำงานของขนาดตัวถูกดำเนินการ 8 และ 16 บิตเป็นสิ่งที่แปลก ความบ้าคลั่งในการพึ่งพาเป็นสาเหตุหนึ่งที่หลีกเลี่ยงคำแนะนำ 16 บิตในขณะนี้ x86-64 ได้รับสิ่งนี้มาจาก 8086 สำหรับ 8 บิตและ 386 สำหรับ 16 บิตและตัดสินใจให้รีจิสเตอร์ 8 และ 16 บิตทำงานในลักษณะเดียวกันในโหมด 64 บิตเหมือนกับที่ทำในโหมด 32 บิต


ดูเพิ่มเติมเหตุใด GCC จึงไม่ใช้การลงทะเบียนบางส่วน สำหรับรายละเอียดที่เป็นประโยชน์เกี่ยวกับวิธีการเขียนลงทะเบียนบางส่วน 8 และ 16 บิต (และการอ่านการลงทะเบียนแบบเต็มในภายหลัง) จะได้รับการจัดการโดย CPU จริง


8
ฉันไม่คิดว่ามันแปลกฉันคิดว่าพวกเขาไม่ต้องการที่จะทำลายมากเกินไปและเก็บพฤติกรรมเก่าไว้ที่นั่น
Alexey Frunze

5
@Alex เมื่อพวกเขาเปิดตัวโหมด 32 บิตไม่มีพฤติกรรมเก่าสำหรับส่วนสูง ไม่มีส่วนสูงมาก่อน .. แน่นอนว่าหลังจากนั้นก็ไม่สามารถเปลี่ยนแปลงได้อีกต่อไป
harold

1
ฉันกำลังพูดถึงตัวถูกดำเนินการ 16 บิตทำไมบิตบนสุดจึงไม่เป็นศูนย์ในกรณีนั้น พวกเขาไม่ได้อยู่ในโหมดที่ไม่ใช่ 64 บิต และยังคงอยู่ในโหมด 64 บิตด้วย
Alexey Frunze

3
ฉันตีความ "พฤติกรรมสำหรับคำสั่ง 16 บิตเป็นสิ่งที่แปลก" เป็น "มันแปลกที่การขยายศูนย์ไม่เกิดขึ้นกับตัวถูกดำเนินการ 16 บิตในโหมด 64 บิต" ดังนั้นความคิดเห็นของฉันเกี่ยวกับการทำให้มันเหมือนเดิมในโหมด 64 บิตเพื่อความเข้ากันได้ที่ดีขึ้น
Alexey Frunze

8
@ อเล็กซ์โอ้ฉันเห็น ตกลง. ฉันไม่คิดว่ามันแปลกจากมุมมองนั้น จากการ "มองย้อนกลับไปบางทีมันอาจจะไม่ใช่ความคิดที่ดีนัก" ก็ตาม เดาว่าน่าจะชัดเจนกว่านี้ :)
harold

9

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

นอกจากนี้ยังช่วยให้คุณไม่ต้องเข้ารหัสค่า 8 ไบต์สำหรับ MOV RAX, 42เมื่อMOV EAX, 42สามารถนำกลับมาใช้ใหม่ได้

การเพิ่มประสิทธิภาพนี้ไม่สำคัญสำหรับตัวเลือก 8 และ 16 บิต (เนื่องจากมีขนาดเล็กกว่า) และการเปลี่ยนกฎก็จะทำลายโค้ดเก่าด้วย


7
ถ้าถูกต้องมันจะไม่สมเหตุสมผลมากกว่าที่จะลงชื่อ - ขยายแทนที่จะเป็น 0 ขยาย
Damien_The_Unbeliever

16
ส่วนขยาย Sign ทำงานช้ากว่าแม้ในฮาร์ดแวร์ การขยายศูนย์สามารถทำได้ควบคู่ไปกับการคำนวณใด ๆ ก็ตามที่สร้างครึ่งล่าง แต่ส่วนขยายเครื่องหมายไม่สามารถทำได้จนกว่าจะคำนวณ (อย่างน้อยเครื่องหมายของ) ครึ่งล่างแล้ว
Jerry Coffin

13
เคล็ดลับอื่นที่เกี่ยวข้องคือการใช้XOR EAX, EAXเพราะXOR RAX, RAXจะต้องมีคำนำหน้า REX
Neil

3
@Nubok: แน่นอนว่าพวกเขาสามารถเพิ่มการเข้ารหัสของ movzx / movsx ที่ใช้อาร์กิวเมนต์ได้ทันที โดยส่วนใหญ่จะสะดวกกว่าในการกำหนดให้บิตด้านบนเป็นศูนย์ดังนั้นคุณสามารถใช้ค่าเป็นดัชนีอาร์เรย์ได้ (เนื่องจาก reg ทั้งหมดต้องมีขนาดเท่ากันในที่อยู่ที่มีประสิทธิภาพ: [rsi + edx]ไม่อนุญาต) แน่นอนว่าการหลีกเลี่ยงการพึ่งพาที่ผิดพลาด / แผงขายของบางส่วน (คำตอบอื่น ๆ ) เป็นอีกเหตุผลหลัก
Peter Cordes

4
และการเปลี่ยนกฎก็จะทำลายรหัสเดิมด้วย รหัสเก่าไม่สามารถทำงานในโหมด 64 บิตได้ (เช่น 1 ไบต์ inc / dec เป็นคำนำหน้า REX) สิ่งนี้ไม่เกี่ยวข้อง เหตุผลในการไม่ทำความสะอาดหูด x86 คือความแตกต่างน้อยกว่าระหว่างโหมดยาวและโหมดความเข้ากันได้ / โหมดเดิมดังนั้นคำแนะนำที่น้อยลงจึงต้องถอดรหัสแตกต่างกันไปขึ้นอยู่กับโหมด AMD ไม่รู้ว่า AMD64 กำลังจะตามมาและน่าเสียดายมากที่อนุรักษ์นิยมดังนั้นจึงต้องใช้ทรานซิสเตอร์น้อยลงเพื่อรองรับ ในระยะยาวมันจะดีถ้าคอมไพเลอร์และมนุษย์ต้องจำว่าสิ่งใดทำงานแตกต่างกันในโหมด 64 บิต
Peter Cordes

1

หากไม่มีศูนย์ขยายเป็น 64 บิตจะหมายถึงการอ่านคำสั่งจากraxจะมีการอ้างอิง 2 รายการสำหรับraxตัวถูกดำเนินการ (คำสั่งที่เขียนถึงeaxและคำสั่งที่เขียนraxก่อนหน้านั้น) หมายความว่า 1) ROB จะต้องมีรายการสำหรับ การพึ่งพาหลายตัวสำหรับตัวถูกดำเนินการเดียวซึ่งหมายความว่า ROB จะต้องใช้ลอจิกและทรานซิสเตอร์มากขึ้นและใช้พื้นที่มากขึ้นและการดำเนินการจะช้าลงในการรอการพึ่งพาครั้งที่สองที่ไม่จำเป็นซึ่งอาจใช้เวลานานในการดำเนินการ หรืออีกทางเลือกหนึ่ง 2) ซึ่งฉันคาดเดาว่าเกิดขึ้นพร้อมกับคำสั่ง 16 บิตขั้นตอนการจัดสรรอาจหยุดชะงัก (กล่าวคือถ้า RAT มีการจัดสรรที่ใช้งานอยู่สำหรับการaxเขียนและการeaxอ่านปรากฏขึ้นมันจะหยุดจนกว่าการaxเขียนจะหยุด)

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

ประโยชน์เพียงอย่างเดียวของการไม่ขยายศูนย์คือการตรวจสอบให้แน่ใจว่าraxมีการรวมบิตคำสั่งซื้อที่สูงขึ้นเช่นหากเดิมมี 0xffffffffffffffff ผลลัพธ์จะเป็น 0xffffffff00000007 แต่ ISA มีเหตุผลน้อยมากที่จะให้การรับประกันนี้ด้วยค่าใช้จ่ายดังกล่าวและ mov rax, 0ก็มีแนวโน้มว่าผลประโยชน์ของการขยายศูนย์จะจริงจะต้องมากขึ้นดังนั้นมันจะช่วยประหยัดสายพิเศษของรหัส ด้วยการให้หลักประกันว่ามันจะถูกศูนย์ขยายไปถึง 64 บิตคอมไพเลอร์สามารถทำงานร่วมกับความจริงในใจในขณะที่mov rdx, rax, raxเท่านั้นที่มีการรอการพึ่งพาเดียวซึ่งหมายความว่ามันสามารถเริ่มดำเนินการได้เร็วขึ้นและออกพ้นขึ้นหน่วยปฏิบัติ นอกจากนี้ยังช่วยให้สำนวนศูนย์มีประสิทธิภาพมากขึ้นเช่นxor eax, eaxศูนย์raxโดยไม่ต้องใช้ไบต์ REX


แฟล็กบางส่วนบน Skylake อย่างน้อยก็ทำงานได้โดยมีอินพุตแยกต่างหากสำหรับ CF เทียบกับ SPAZO ใด ๆ ( cmovbeเท่ากับ 2 uops แต่cmovbเป็น 1) แต่ไม่มีซีพียูที่ทำการเปลี่ยนชื่อการลงทะเบียนบางส่วนทำตามที่คุณแนะนำ แต่จะใส่ uop ที่รวมเข้าด้วยกันหากมีการเปลี่ยนชื่อ reg บางส่วนแยกจาก reg ทั้งหมด (กล่าวคือ "สกปรก") ดูเหตุใด GCC จึงไม่ใช้การลงทะเบียนบางส่วน และการลงทะเบียนบางส่วนบน Haswell / Skylake ทำงานอย่างไร? การเขียน AL ดูเหมือนจะมีการพึ่งพา RAX ที่ผิดพลาดและ AH ไม่สอดคล้องกัน
Peter Cordes

ซีพียูตระกูล P6 หยุดทำงานเป็นเวลา ~ 3 รอบเพื่อแทรก uop ที่ผสาน (Core2 / Nehalem) หรือตระกูล P6 รุ่นก่อนหน้า (PM, PIII, PII, PPro) เพียงแค่หยุด (อย่างน้อย?) ~ 6 รอบ อาจจะเป็นเหมือนที่คุณแนะนำใน 2 รอให้ค่า reg เต็มพร้อมใช้งานผ่านการเขียนกลับไปยังไฟล์ทะเบียนถาวร / สถาปัตยกรรม
Peter Cordes

@PeterCordes โอ้ฉันรู้เกี่ยวกับการรวม uops อย่างน้อยสำหรับแผงขายธงบางส่วน มีเหตุผล แต่ฉันลืมไปว่ามันทำงานอย่างไร มันคลิกหนึ่งครั้ง แต่ฉันลืมจดบันทึก
Lewis Kelsey

@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXฉันไม่พบตัวอย่างของ 'การรวม uop' ที่จะใช้ในการแก้ปัญหานี้แม้ว่าจะเหมือนกับแผงขายธงบางส่วนก็ตาม
Lewis Kelsey

ใช่แล้ว P6 ในช่วงต้นจะหยุดจนกว่าจะมีการเขียนคืน Core2 และ Nehalem ใส่ uop ผสานหลัง / ก่อน? เพียงถ่วงส่วนหน้าเป็นเวลาสั้นลง Sandybridge แทรก uops ที่รวมเข้าด้วยกันโดยไม่ต้องหยุดชะงัก (แต่การรวม AH จะต้องออกเป็นวงจรด้วยตัวเองในขณะที่การรวม AL อาจเป็นส่วนหนึ่งของกลุ่มเต็มได้) Haswell / SKL ไม่ได้เปลี่ยนชื่อ AL แยกจาก RAX เลยดังนั้นจึงmov al, [mem]เป็นโหลดไมโครฟิวส์ + ALU- ผสานการเปลี่ยนชื่อ AH เท่านั้นและการรวม AH ยังคงเป็นปัญหาเพียงอย่างเดียว กลไกการรวมแฟล็กบางส่วนในซีพียูเหล่านี้แตกต่างกันไปเช่น Core2 / Nehalem ยังคงหยุดเพียงบางส่วนสำหรับแฟล็กบางส่วนซึ่งแตกต่างจาก partial-reg
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.