Chức năng mã máy x86-64, 30 byte.
Sử dụng logic đệ quy tương tự như câu trả lời C của @Level River St . (Độ sâu đệ quy tối đa = 100)
Sử dụng puts(3)
chức năng từ libc, mà các tệp thực thi thông thường được liên kết với dù sao đi nữa. Nó có thể gọi được bằng cách sử dụng Hệ thống V86I x86-64, tức là từ C trên Linux hoặc OS X, và không ghi đè bất kỳ thanh ghi nào mà nó không được yêu cầu.
objdump -drwC -Mintel
đầu ra, nhận xét với lời giải thích
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.
Được xây dựng với yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
. Tôi có thể đăng nguồn NASM ban đầu, nhưng điều đó có vẻ như lộn xộn vì các hướng dẫn asm có ngay trong phần tháo gỡ.
putchar@plt
là ít hơn 128 byte từ jl
, vì vậy tôi có thể đã sử dụng bước nhảy ngắn 2 byte thay vì bước nhảy gần 6 byte, nhưng điều đó chỉ đúng trong một chương trình thực thi nhỏ, không phải là một phần của chương trình lớn hơn. Vì vậy, tôi không nghĩ rằng tôi có thể biện minh cho việc không tính quy mô của việc thực hiện libc nếu tôi cũng tận dụng mã hóa jcc ngắn để tiếp cận nó.
Mỗi cấp độ đệ quy sử dụng 24B không gian ngăn xếp (2 lần đẩy và địa chỉ trả lại được đẩy bằng CALL). Mọi độ sâu khác sẽ gọi putchar
với ngăn xếp chỉ được căn chỉnh bởi 8, không phải 16, do đó, điều này vi phạm ABI. Việc triển khai stdio sử dụng các cửa hàng được căn chỉnh để làm đổ các thanh ghi xmm vào ngăn xếp sẽ bị lỗi. Nhưng glibc putchar
không làm điều đó, viết vào một đường ống với bộ đệm đầy đủ hoặc viết vào một thiết bị đầu cuối với bộ đệm dòng. Đã thử nghiệm trên Ubuntu 15.10. Điều này có thể được khắc phục bằng một lần đẩy / bật giả trong .loop
, để bù cho ngăn xếp thêm 8 lần nữa trước cuộc gọi đệ quy.
Bằng chứng là nó in đúng số dòng mới:
# 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.
Phiên bản đầu tiên của tôi là 43B và được sử dụng puts()
trên bộ đệm gồm 9 dòng mới (và chấm dứt 0 byte), do đó, sẽ đặt thêm số 10. Trường hợp cơ sở đệ quy đó thậm chí còn gần hơn với cảm hứng C.
Bao thanh toán 10 ^ 100 một cách khác nhau có thể đã rút ngắn bộ đệm, có thể giảm xuống còn 4 dòng mới, tiết kiệm 5 byte, nhưng sử dụng putchar thì tốt hơn nhiều. Nó chỉ cần một số nguyên arg, không phải là một con trỏ và không có bộ đệm nào cả. Tiêu chuẩn C cho phép triển khai trong đó có macro putc(val, stdout)
, nhưng trong glibc, nó tồn tại như một chức năng thực sự mà bạn có thể gọi từ asm.
Chỉ in một dòng mới cho mỗi cuộc gọi thay vì 10 chỉ có nghĩa là chúng ta cần tăng độ sâu đệ quy tối đa thêm 1, để có thêm một hệ số 10 dòng mới. Vì cả 99 và 100 đều có thể được biểu diễn bằng 8 bit ngay lập tức push 100
được ký hiệu, vẫn chỉ là 2 byte.
Thậm chí tốt hơn, có 10
trong một thanh ghi hoạt động như cả một dòng mới và bộ đếm vòng lặp, tiết kiệm một byte.
Ý tưởng để lưu byte
Phiên bản 32 bit có thể tiết kiệm một byte cho dec edi
, nhưng quy ước gọi stack-args (đối với các hàm thư viện như putchar) làm cho công việc gọi đuôi dễ dàng hơn và có thể sẽ cần nhiều byte hơn ở nhiều nơi hơn. Tôi có thể sử dụng một quy ước register-arg cho riêng tư f()
, chỉ được gọi bởi g()
, nhưng sau đó tôi không thể gọi putar (vì f () và putchar () sẽ có số lượng stack-args khác nhau).
Có thể có f () duy trì trạng thái của người gọi, thay vì thực hiện lưu / khôi phục trong trình gọi. Tuy nhiên, điều đó có thể rất tệ bởi vì có lẽ nó cần phải tách riêng ở mỗi bên của chi nhánh và không tương thích với việc cắt đuôi. Tôi đã thử nó nhưng không tìm thấy bất kỳ khoản tiết kiệm.
Giữ một bộ đếm vòng lặp trên ngăn xếp (thay vì đẩy / bật RCx trong vòng lặp) cũng không giúp được gì. Đó là 1B tồi tệ hơn với phiên bản được sử dụng, và có lẽ còn thua lỗ hơn với phiên bản này thiết lập RCx rẻ hơn.