x86-64 makine kodu işlevi, 30 bayt.
@Level River St. C ile aynı özyineleme mantığını kullanır . (Maksimum özyineleme derinliği = 100)
puts(3)
Yine de normal çalıştırılabilir dosyalara karşı bağlı olan libc işlevini kullanır . X86-64 System V ABI kullanılarak, yani Linux'tan veya OS X'deki C'den çağrılabilir ve olması gerekmeyen kayıtları engellemez.
objdump -drwC -Mintel
çıktı, açıklama ile yorumlandı
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.
İle oluşturulmuş yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
. Orijinal NASM kaynağını gönderebilirim, ancak asm talimatları orada demontajda olduğu için karışıklık gibiydi.
putchar@plt
ondan 128 bayttan daha az jl
, bu nedenle 6 bayt yakınına atlamak yerine 2 bayt kısa atlamayı kullanabilirdim, ancak bu daha büyük bir programın parçası olarak değil, sadece küçük bir çalıştırılabilir dosya için geçerlidir. Bu yüzden, eğer ulaşmak için kısa bir jcc kodlaması kullanmamdan faydalanırsam, libc'nin uygulama koyduğunun büyüklüğünü saymamayı haklı çıkaracağımı sanmıyorum.
Her tekrarlama seviyesi, 24B yığın alanı kullanır (2 basış ve CALL tarafından basılan dönüş adresi). Diğer her derinlik putchar
istif 16 ile değil, 8 ile aynı hizada olacak ve böylece ABI'yi ihlal edecek. Xmm kayıtlarını yığına dökmek için hizalanmış depoları kullanan bir stdio uygulaması hata verir. Fakat glibc'ler putchar
bunu yapmaz, tam tamponlama ile bir boruya yazma veya satır tamponlama ile bir terminale yazma. Ubuntu 15.10'da test edilmiştir. Bu .loop
, özyinelemeli çağrıdan önce yığını 8 ile dengelemek için, kukla bir itme / patlatma ile sabitlenebilir .
Doğru sayıda yeni satır yazdırdığını gösteren kanıt:
# 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.
Bunun ilk versiyonum 43B idi ve kullanılmış puts()
9 yeni satırlık bir tamponda (ve 0 bitlik bir sonlandırmada) kullanıldı, bu yüzden puts 10'unu ekleyecekti. Bu özyinelemenin temel durumu C ilhamına daha da yakındı.
10 ^ 100 faktoring farklı bir yolla, arabellek kısaltabilir, belki 4 yeni satıra indirebilir, 5 bayt tasarruf edebilir, ancak putchar kullanmak daha iyidir. Sadece bir tamsayı argümanına ihtiyaç duyar, bir işaretçi değil, hiç tampon içermez. C standardı bir makronun bulunduğu uygulamalara izin verir putc(val, stdout)
, ancak glibc'de asm'den çağırabileceğiniz gerçek bir fonksiyon olarak mevcuttur.
Çağrı başına 10 yerine sadece bir yeni satır yazdırmak, 10 yeni satırın başka bir faktörünü elde etmek için özyineleme derinliğini 1 arttırmamız gerektiği anlamına gelir. 99 ve 100'ün ikisi de 8-bitlik bir işaret genişletilmiş işaretiyle gösterilebildiğinden beri push 100
, hala sadece 2 bayttır.
Daha da iyisi, 10
bir kayıt defterine sahip olmak hem yeni bir satır hem de bir döngü sayacı olarak çalışarak bir bayt tasarrufu sağlar.
Bayt tasarrufu için fikirler
32 bitlik bir sürüm, bayt için tasarruf sağlayabilir dec edi
, ancak yığın çağrısı çağırma kuralı (putchar gibi kütüphane işlevleri için), kuyruk çağrısı işini daha az kolay yapar ve muhtemelen daha fazla yerde daha fazla bayt gerektirir. Özel için register-arg kuralını kullanabildim f()
, sadece çağırdım g()
, ama sonra kuyruk çağrısı yapamadım (çünkü f () ve putchar () farklı sayıda yığın arşivi alacaktır).
Arayana kaydetme / geri yükleme yapmak yerine f () işlevinin arayanın durumunu koruması mümkün olacaktır. Bu muhtemelen berbattır, çünkü muhtemelen dalın her iki tarafına ayrı ayrı girmesi gerekir ve kuyruk çağırma ile uyumlu değildir. Denedim ama tasarruf bulamadım.
Bir döngü sayacını yığında tutmak (döngüdeki push / popping rcx yerine) da yardımcı olmadı. Kullanılan sürümü ile 1B daha kötüydü ve rcx'i daha ucuza ayarlayan bu sürümde muhtemelen daha da büyük bir kayıp meydana geldi.