Функция машинного кода 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. Это можно исправить с помощью фиктивного push / pop в .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 намного лучше. Для этого требуется только целочисленный аргумент arg, а не указатель и вообще никакого буфера. Стандарт 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 дешевле.