ชุดประกอบ x86, 9 ไบต์ (สำหรับการแข่งขัน)
ทุกคนที่พยายามใช้ความท้าทายนี้ในภาษาระดับสูงนั้นพลาดไม่ได้กับความสนุกที่แท้จริงของการจัดการบิตดิบ มีวิธีการทำสิ่งต่าง ๆ ที่หลากหลายมากมายมันบ้าและสนุกมากที่จะคิด ต่อไปนี้เป็นคำตอบบางส่วนที่ฉันคิดในภาษาแอสเซมบลี x86 แบบ 32 บิต
ฉันขอโทษล่วงหน้าว่านี่ไม่ใช่คำตอบของ code-golf ทั่วไป ฉันจะเดินเล่นมากเกี่ยวกับกระบวนการคิดของการเพิ่มประสิทธิภาพซ้ำ (สำหรับขนาด) หวังว่าจะน่าสนใจและให้ความรู้แก่ผู้ชมจำนวนมาก แต่ถ้าคุณเป็นประเภท TL; DR ฉันจะไม่โกรธถ้าคุณข้ามไปจนจบ
วิธีแก้ปัญหาที่ชัดเจนและมีประสิทธิภาพคือการทดสอบว่าค่าเป็นเลขคี่หรือคู่ (ซึ่งสามารถทำได้อย่างมีประสิทธิภาพโดยการดูบิตที่มีนัยสำคัญน้อยที่สุด) จากนั้นเลือกระหว่างn + 1หรือn. 1ตามลำดับ สมมติว่าอินพุตถูกส่งผ่านเป็นพารามิเตอร์ในECX
รีจิสเตอร์และผลลัพธ์ถูกส่งคืนในEAX
รีจิสเตอร์เราจะได้รับฟังก์ชั่นต่อไปนี้:
F6 C1 01 | test cl, 1 ; test last bit to see if odd or even
8D 41 01 | lea eax, DWORD PTR [ecx + 1] ; set EAX to n+1 (without clobbering flags)
8D 49 FF | lea ecx, DWORD PTR [ecx - 1] ; set ECX to n-1 (without clobbering flags)
0F 44 C1 | cmovz eax, ecx ; move in different result if input was even
C3 | ret
(13 ไบต์)
แต่สำหรับ code-golf LEA
คำแนะนำเหล่านั้นไม่ดีนักเนื่องจากใช้เวลาในการเข้ารหัส 3 ไบต์ การเรียบง่ายแบบDEC
ใหม่ECX
จะสั้นกว่ามาก (เพียงหนึ่งไบต์) แต่สิ่งนี้มีผลต่อการตั้งค่าสถานะดังนั้นเราจึงต้องมีความชาญฉลาดเล็กน้อยในการจัดเรียงรหัส เราสามารถทำพร่องแรกและคู่ / คี่ทดสอบที่สองแต่แล้วเราจะต้องกลับผลของการทดสอบคี่ / แม้กระทั่ง
นอกจากนี้เรายังสามารถเปลี่ยนคำสั่งย้ายแบบมีเงื่อนไขเป็นสาขาซึ่งอาจทำให้โค้ดทำงานช้าลง (ขึ้นอยู่กับว่าสาขานั้นสามารถคาดการณ์ได้อย่างไร - ถ้าอินพุตสลับกันไม่สอดคล้องกันระหว่างคี่และคู่สาขาจะช้าลงหากมี รูปแบบก็จะเร็วขึ้น) ซึ่งจะช่วยเราไบต์อื่น
ในความเป็นจริงด้วยการแก้ไขนี้การดำเนินการทั้งหมดสามารถทำได้ในสถานที่โดยใช้เพียงการลงทะเบียนเดียว นี่เป็นสิ่งที่ดีหากคุณฝังโค้ดนี้ไว้ที่ใดที่หนึ่ง (และโอกาสที่คุณจะเป็นเพราะมันสั้นมาก)
48 | dec eax ; decrement first
A8 01 | test al, 1 ; test last bit to see if odd or even
75 02 | jnz InputWasEven ; (decrement means test result is inverted)
40 | inc eax ; undo the decrement...
40 | inc eax ; ...and add 1
InputWasEven: ; (two 1-byte INCs are shorter than one 3-byte ADD with 2)
(inlined: 7 ไบต์; เป็นฟังก์ชัน: 10 ไบต์)
แต่ถ้าคุณต้องการให้มันเป็นฟังก์ชั่นล่ะ? ไม่มีการเรียกแบบแผนมาตรฐานที่ใช้การลงทะเบียนเดียวกันเพื่อส่งผ่านพารามิเตอร์เช่นเดียวกับค่าส่งคืนดังนั้นคุณจะต้องเพิ่มMOV
คำสั่งการลงทะเบียนลงทะเบียนที่จุดเริ่มต้นหรือจุดสิ้นสุดของฟังก์ชัน นี่แทบไม่มีค่าใช้จ่ายในเรื่องความเร็ว แต่มันเพิ่ม 2 ไบต์ ( RET
คำสั่งนี้ยังเพิ่มไบต์และมีค่าใช้จ่ายที่แนะนำโดยความต้องการที่จะทำและกลับมาจากการเรียกใช้ฟังก์ชั่นซึ่งหมายความว่านี่คือตัวอย่างหนึ่งที่อินไลน์ทำให้เกิดประโยชน์ทั้งความเร็วและขนาดแทนที่จะเป็นความเร็วแบบคลาสสิก การแลกเปลี่ยนพื้นที่สำหรับ.) ในทั้งหมดเขียนเป็นฟังก์ชั่นรหัสนี้ขยายตัวถึง 10 ไบต์
เราสามารถทำอะไรได้อีกใน 10 ไบต์? ถ้าเราใส่ใจเรื่องประสิทธิภาพ (อย่างน้อยก็คาดเดาได้ ) มันคงจะดีถ้าได้กำจัดสาขานั้น นี่เป็นวิธีการแก้ปัญหากิ่งก้านสาขาที่มีขนาดเท่าไบต์ หลักฐานขั้นพื้นฐานนั้นง่าย: เราใช้บิต XOR เพื่อพลิกบิตสุดท้ายแปลงค่าแปลก ๆ ให้เป็นเลขคู่และในทางกลับกัน แต่มี niggle หนึ่งอัน - สำหรับอินพุตแปลก ๆ , ที่ให้n-1แก่เรา, ในขณะที่สำหรับอินพุตแม้แต่, มันให้n + 1 เรา - ตรงข้ามกับสิ่งที่เราต้องการ ดังนั้นเพื่อแก้ไขปัญหานั้นเราดำเนินการกับค่าลบพลิกเครื่องหมายอย่างมีประสิทธิภาพ
8B C1 | mov eax, ecx ; copy parameter (ECX) to return register (EAX)
|
F7 D8 | neg eax ; two's-complement negation
83 F0 01 | xor eax, 1 ; XOR last bit to invert odd/even
F7 D8 | neg eax ; two's-complement negation
|
C3 | ret ; return from function
(inlined: 7 ไบต์; เป็นฟังก์ชัน: 10 ไบต์)
เนียนสวย เป็นการยากที่จะทราบว่าสามารถปรับปรุงได้อย่างไร สิ่งหนึ่งดึงดูดสายตาของฉัน: NEG
คำแนะนำ2 ไบต์ทั้งสองนี้ ตรงไปตรงมาสองไบต์ดูเหมือนว่าหนึ่งไบต์มากเกินไปที่จะเข้ารหัสการปฏิเสธอย่างง่าย ๆ แต่นั่นเป็นชุดคำสั่งที่เราต้องทำงานด้วย มีวิธีแก้ไขปัญหาหรือไม่? แน่นอน! ถ้าเราXOR
คูณ -2 เราสามารถแทนที่NEG
ation ตัวที่สองด้วยINC
rement:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(inlined: 6 ไบต์; เป็นฟังก์ชัน: 9 ไบต์)
หนึ่งในความแปลกประหลาดของชุดคำสั่ง x86 ก็คือคำสั่งอเนกประสงค์LEA
ซึ่งสามารถทำการย้ายรีจิสเตอร์ลงทะเบียนการเพิ่มรีจิสเตอร์รีจิสเตอร์ชดเชยด้วยค่าคงที่และปรับขนาดทั้งหมดในคำสั่งเดียว!
8B C1 | mov eax, ecx
83 E0 01 | and eax, 1 ; set EAX to 1 if even, or 0 if odd
8D 44 41 FF | lea eax, DWORD PTR [ecx + eax*2 - 1]
C3 | ret
(10 ไบต์)
AND
การเรียนการสอนเป็นเหมือนTEST
การเรียนการสอนที่เราใช้ก่อนหน้านี้ในการที่ทั้งสองทำบิต-AND และชุดธงตาม แต่AND
จริง ๆ การปรับปรุงตัวถูกดำเนินปลายทาง LEA
การเรียนการสอนแล้วตาชั่งนี้โดย 2 เพิ่มมูลค่าการป้อนข้อมูลเดิมและลดลงทีละ 1 โดยหากค่าการป้อนข้อมูลที่เป็นเลขคี่หัก 1 (2 × 0-1 = -1) จากมัน; ถ้าค่าอินพุตเป็นเลขคู่จะเพิ่ม 1 (2 × 1 - 1 = 1) ลงไป
นี่เป็นวิธีที่รวดเร็วและมีประสิทธิภาพในการเขียนโค้ดเนื่องจากการประมวลผลส่วนใหญ่สามารถทำได้ในส่วนหน้า แต่ก็ไม่ได้ซื้อเรามากนักในทางไบต์เนื่องจากใช้การเข้ารหัสซับซ้อนLEA
คำแนะนำ. รุ่นนี้ยังใช้งานไม่ได้กับจุดประสงค์ในการอินไลน์เนื่องจากมันต้องการให้ค่าอินพุตดั้งเดิมถูกสงวนไว้เป็นอินพุตของLEA
คำสั่ง ดังนั้นด้วยความพยายามในการเพิ่มประสิทธิภาพครั้งล่าสุดนี้เราจึงย้อนกลับไปตามความเป็นจริงโดยบอกว่าอาจถึงเวลาหยุด
ดังนั้นสำหรับการแข่งขันรายการสุดท้ายเรามีฟังก์ชั่น 9- ไบต์ที่รับค่าอินพุตในECX
รีจิสเตอร์ (แบบแผนการเรียกใช้การลงทะเบียนแบบกึ่งมาตรฐานบน 32- บิต x86) และส่งกลับผลลัพธ์ในEAX
รีจิสเตอร์ (เช่นเดียวกับ ประชุม x86 ทั้งหมด):
SwapParity PROC
8B C1 mov eax, ecx
F7 D8 neg eax
83 F0 FE xor eax, -2
40 inc eax
C3 ret
SwapParity ENDP
พร้อมที่จะประกอบกับ MASM; โทรจาก C เป็น:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU