x86-64 기계 코드 기능, 30 바이트
@Level River St의 C 답변 과 동일한 재귀 논리를 사용합니다 . (최대 재귀 깊이 = 100)
puts(3)
일반 실행 파일이 어쨌든 연결되는 libc 의 함수를 사용합니다 . x86-64 System V ABI, 즉 Linux 또는 OS X의 C에서 호출 할 수 있으며 원하지 않는 레지스터는 클로버하지 않습니다.
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
이므로 6 바이트 니어 점프 대신 2 바이트 짧은 점프를 사용할 수 있었지만 더 큰 프로그램의 일부가 아닌 작은 실행 파일에서만 유효합니다. 따라서 짧은 jcc 인코딩을 사용하여 libc의 puts 구현 크기를 계산하지 않으면 정당화 할 수 없다고 생각합니다.
각 재귀 수준은 24B의 스택 공간을 사용합니다 (2 개의 푸시 및 CALL에 의해 리턴 된 리턴 주소). 다른 모든 깊이는 putchar
스택이 16이 아닌 8로만 정렬되어 호출 되므로 ABI를 위반합니다. 정렬 된 저장소를 사용하여 xmm 레지스터를 스택에 유출하는 stdio 구현은 오류가 발생했습니다. 그러나 glibc putchar
는 풀 버퍼링으로 파이프에 쓰거나 라인 버퍼링으로 터미널에 쓰지 않습니다. 우분투 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 바이트)의 버퍼에서 사용 되므로 puts는 10 번째를 추가합니다. 이 재귀 사례는 C 영감에 더 가깝습니다.
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과 같은 라이브러리 함수의 경우 스택 인수 호출 규칙을 사용하면 테일 호출이 덜 쉬워지고 더 많은 장소에서 더 많은 바이트가 필요할 수 있습니다. private에 대해 register-arg 규칙을 사용할 수 있지만 f()
로만 호출 g()
할 수 있지만 putchar를 꼬리 호출 할 수는 없습니다 (f () 및 putchar ()는 다른 수의 스택 인수를 사용하기 때문에).
호출자에서 저장 / 복원을 수행하는 대신 f ()가 호출자의 상태를 유지하도록 할 수 있습니다. 분기의 양쪽에서 별도로 가져와야하며 테일 콜링과 호환되지 않기 때문에 아마도 짜증납니다. 나는 그것을 시도했지만 저축을 찾지 못했습니다.
루프에서 푸시 / 팝핑 rcx 대신 스택에 루프 카운터를 유지하는 것도 도움이되지 않았습니다. 사용하는 버전이 1B 나빠졌으며 rcx를보다 저렴하게 설정하는이 버전의 손실이 훨씬 많았습니다.