Функція машинного коду x86-64, 30 байт.
Використовує ту ж логіку рекурсії як відповідь За допомогою @Level River St . (Максимальна глибина рекурсії = 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 натискання та зворотну адресу, натиснуту 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-ту. Ця база рекурсії була ще ближче до натхнення С.
Факторинг 10 ^ 100 по-іншому, можливо, може скоротити буфер, можливо, до 4 нових рядків, заощадивши 5 байт, але використовувати putchar краще далеко. Потрібен лише цілий аргумент, а не вказівник і зовсім не буфер. Стандарт C дозволяє реалізувати, де це макрос putc(val, stdout)
, але в glibc він існує як реальна функція, яку можна викликати з ASM.
Друк лише однієї нової лінії за виклик замість 10 просто означає, що нам потрібно збільшити максимальну глибину рекурсії на 1, щоб отримати ще один коефіцієнт 10 нових рядків. Так як 99 і 100 можуть бути представлені знаком, розширеним 8-бітовим безпосередньо, push 100
все одно лише 2 байти.
Ще краще, мати 10
реєстр працює як лінійкою нового рядка, так і лічильником циклу, зберігаючи байт.
Ідеї для збереження байтів
32-розрядна версія може зберегти байт для dec edi
, але конвенція про виклик стеків (для функцій бібліотеки, таких як putchar) робить роботу хвостового виклику менш легкою, і, ймовірно, знадобиться більше байтів у більшій кількості місць. Я міг би використовувати конвенцію на регістр-аргумент для приватного f()
, який викликав тільки g()
, але тоді я не міг хвостового виклику putchar (тому що f () і putchar () брали б різну кількість аргументів стека).
Можна було б зберегти стан абонента f (), а не робити збереження / відновлення в абонента. Це, мабуть, відстійно, тому що, мабуть, потрібно буде потрапляти окремо в кожну сторону гілки, і це не сумісно з хвостовим звоном. Я спробував це, але не знайшов жодної економії.
Утримання лічильника циклу на стеку (замість push / popping rcx у циклі) також не допомогло. Це було на 1B гірше з використовуваною версією, і, ймовірно, ще більше втрат у цій версії, яка встановлює rcx дешевше.