ฟังก์ชั่นรหัสเครื่อง x86-64 ขนาด 30 ไบต์
ใช้ตรรกะการเรียกซ้ำเช่นเดียวกับคำตอบที่ C โดย @Level แม่น้ำเซนต์ (ความลึกสูงสุดของการเรียกซ้ำ = 100)
ใช้puts(3)
ฟังก์ชั่นจาก libc ซึ่งไฟล์เอ็กซีคิ้วท์ปกติเชื่อมโยงกันอยู่แล้ว มันเรียกได้ว่าใช้ x86-64 System V ABI เช่นจาก C บน Linux หรือ OS X และไม่ได้ปิดบังการลงทะเบียนใด ๆ ที่ไม่ควรทำ
objdump -drwC -Mintel
เอาท์พุทแสดงความคิดเห็นพร้อมคำอธิบาย
0000000000400340 <g>: ## wrapper function
400340: 6a 64 push 0x64
400342: 5f pop rdi ; mov edi, 100 in 3 bytes instead of 5
; tailcall f by falling into it.
0000000000400343 <f>: ## the recursive function
400343: ff cf dec edi
400345: 97 xchg edi,eax
400346: 6a 0a push 0xa
400348: 5f pop rdi ; mov edi, 10
400349: 0f 8c d1 ff ff ff jl 400320 <putchar> # conditional tailcall
; if we don't tailcall, then eax=--n = arg for next recursion depth, and edi = 10 = '\n'
40034f: 89 f9 mov ecx,edi ; loop count = the ASCII code for newline; saves us one byte
0000000000400351 <f.loop>:
400351: 50 push rax ; save local state
400352: 51 push rcx
400353: 97 xchg edi,eax ; arg goes in rdi
400354: e8 ea ff ff ff call 400343 <f>
400359: 59 pop rcx ; and restore it after recursing
40035a: 58 pop rax
40035b: e2 f4 loop 400351 <f.loop>
40035d: c3 ret
# the function ends here
000000000040035e <_start>:
0x040035e - 0x0400340 = 30 bytes
# not counted: a caller that passes argc-1 to f() instead of calling g
000000000040035e <_start>:
40035e: 8b 3c 24 mov edi,DWORD PTR [rsp]
400361: ff cf dec edi
400363: e8 db ff ff ff call 400343 <f>
400368: e8 c3 ff ff ff call 400330 <exit@plt> # flush I/O buffers, which the _exit system call (eax=60) doesn't do.
yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
สร้างขึ้นด้วย ฉันสามารถโพสต์ต้นฉบับของ NASM ได้ แต่นั่นดูเหมือนว่าจะยุ่งเหยิงเนื่องจากคำแนะนำของ asm อยู่ที่นั่นในการถอดชิ้นส่วน
putchar@plt
อยู่ห่างจากน้อยกว่า 128 ไบต์jl
ดังนั้นฉันสามารถใช้การกระโดดแบบ 2 ไบต์สั้น ๆ แทนการกระโดดแบบ 6 ไบต์ใกล้ ๆ แต่นั่นเป็นความจริงในไฟล์ปฏิบัติการขนาดเล็กเท่านั้นไม่ใช่เป็นส่วนหนึ่งของโปรแกรมที่มีขนาดใหญ่กว่า ดังนั้นฉันไม่คิดว่าฉันจะพิสูจน์ได้ว่าไม่นับขนาดของการใช้งาน libc หากฉันใช้ประโยชน์จากการเข้ารหัส jcc แบบสั้นเพื่อเข้าถึง
การเรียกซ้ำแต่ละระดับใช้ 24B ของพื้นที่สแต็ก (2 pushes และที่อยู่ผู้ส่งเป็นแบบ CALL) ความลึกอื่น ๆ จะเรียกputchar
ด้วยสแต็คที่จัดเรียงตาม 8 เท่านั้นไม่ใช่ 16 ดังนั้นนี่จึงเป็นการละเมิด ABI การใช้งาน stdio ที่ใช้ร้านค้าที่มีการจัดแนวเพื่อหกลงทะเบียน xmm กับสแต็กจะเป็นความผิด แต่ glibc ก็putchar
ไม่ได้ทำเช่นนั้นเขียนไปที่ไปป์ที่มีบัฟเฟอร์เต็มหรือเขียนไปยังเทอร์มินัลที่มีการบัฟเฟอร์บรรทัด ทดสอบกับ Ubuntu 15.10 สิ่งนี้สามารถแก้ไขได้ด้วยการกด / จำลองแบบดัมมี่ใน.loop
, เพื่อชดเชยสแต็กอีก 8 ก่อนการโทรซ้ำ
พิสูจน์ว่าพิมพ์บรรทัดใหม่จำนวนที่เหมาะสม:
# with a version that uses argc-1 (i.e. the shell's $i) instead of a fixed 100
$ for i in {0..8}; do echo -n "$i: "; ./golf-googol $(seq $i) |wc -c; done
0: 1
1: 10
2: 100
3: 1000
4: 10000
5: 100000
6: 1000000
7: 10000000
8: 100000000
... output = 10^n newlines every time.
รุ่นแรกของฉันคือ 43B และใช้puts()
กับบัฟเฟอร์ของ 9 บรรทัดใหม่ (และยกเลิก 0 ไบต์) ดังนั้นการใส่จะผนวกที่ 10 กรณีฐานการเรียกซ้ำนั้นอยู่ใกล้กับแรงบันดาลใจ C มากขึ้น
แฟคตอริ่ง 10 ^ 100 วิธีที่แตกต่างกันอาจจะทำให้บัฟเฟอร์สั้นลงอาจถึง 4 บรรทัดใหม่ประหยัด 5 ไบต์ แต่การใช้ putchar ดีกว่า มันต้องการ ARG จำนวนเต็มเท่านั้นไม่ใช่ตัวชี้และไม่มีบัฟเฟอร์เลย มาตรฐาน C อนุญาตการใช้งานที่เป็นแมโครputc(val, stdout)
แต่ใน glibc จะมีอยู่เป็นฟังก์ชันจริงที่คุณสามารถโทรจาก asm
การพิมพ์บรรทัดใหม่เพียงครั้งเดียวต่อการโทรแทน 10 หมายถึงเราจำเป็นต้องเพิ่มความลึกสูงสุดของการเรียกซ้ำ 1 เพื่อให้ได้ปัจจัยใหม่อีก 10 บรรทัด ตั้งแต่ 99 และ 100 ทั้งสองสามารถแสดงด้วยสัญญาณขยาย 8 บิตทันทีpush 100
ยังคงเป็นเพียง 2 ไบต์
ยิ่งไปกว่านั้นการมี10
การลงทะเบียนใช้งานได้ทั้ง newline และ loop counter ช่วยประหยัด byte
แนวคิดสำหรับการบันทึกไบต์
เวอร์ชัน 32 บิตสามารถบันทึกไบต์สำหรับdec edi
แต่การเรียก stack-args (สำหรับฟังก์ชันไลบรารีเช่น putchar) ทำให้การโทรแบบหางทำงานได้ง่ายขึ้นน้อยลงและอาจต้องใช้ไบต์มากขึ้นในที่ต่างๆ ฉันสามารถใช้ระเบียบทะเบียน register-arg สำหรับไพรเวตf()
เรียกโดยg()
เท่านั้น แต่จากนั้นฉันไม่สามารถเรียก putchar (เพราะ f () และ putchar () จะใช้จำนวนสแต็ก args ที่แตกต่างกัน
เป็นไปได้ที่จะมี f () คงสถานะผู้โทรไว้แทนที่จะทำการบันทึก / กู้คืนในผู้โทร แต่นั่นอาจจะแย่เพราะมันอาจจะต้องแยกจากกันในแต่ละด้านของสาขาและไม่สามารถใช้งานร่วมกับ tailcalling ฉันลองแล้ว แต่ไม่พบการประหยัดใด ๆ
การรักษาตัวนับลูปบนสแต็ก (แทนที่จะกด / popping rcx ในลูป) ก็ไม่ได้ช่วยอะไรเช่นกัน มันแย่กว่ารุ่นที่ 1B ที่ใช้แล้วและอาจขาดทุนมากกว่ารุ่นนี้ที่ตั้งค่า rcx ให้ถูกกว่า