codice macchina x86 a 32 bit (con chiamate di sistema Linux): 106 105 byte
log delle modifiche: salvato un byte nella versione veloce perché una costante off-by-one non modifica il risultato per Fib (1G).
O 102 byte per una versione più lenta del 18% (su Skylake) (usando mov
/ sub
/ cmc
invece di lea
/ cmp
nel ciclo interno, per generare carry-out e wrapping 10**9
invece di 2**32
). O 101 byte per una versione più lenta ~ 5,3x con un ramo nel carry-handling nel ciclo più interno. (Ho misurato un tasso di violazione del ramo del 25,4%!)
Oppure 104/101 byte se è consentito uno zero iniziale. (Ci vuole 1 byte in più per hard-code saltando 1 cifra dell'output, che è ciò che è necessario per Fib (10 ** 9)).
Sfortunatamente, la modalità NASM di TIO sembra ignorare -felf32
nei flag del compilatore. Ecco comunque un link con il mio codice sorgente completo, con tutta la confusione di idee sperimentali nei commenti.
Questo è un programma completo . Stampa le prime 1000 cifre di Fib (10 ** 9) seguite da alcune cifre extra (le ultime alcune sono errate) seguite da alcuni byte di immondizia (esclusa una nuova riga). La maggior parte della spazzatura è non ASCII, quindi potresti voler passare attraverso cat -v
. konsole
Tuttavia, non rompe il mio emulatore di terminale (KDE ). I "byte di immondizia" stanno memorizzando Fib (999999999). Avevo già -1024
un registro, quindi era più economico stampare 1024 byte della dimensione corretta.
Sto contando solo il codice macchina (dimensione del segmento di testo del mio eseguibile statico), non la lanugine che lo rende un eseguibile ELF. ( Sono possibili eseguibili ELF molto piccoli , ma non volevo preoccuparmene). Si è rivelato più breve utilizzare la memoria dello stack anziché BSS, quindi posso giustificare di non contare nient'altro nel binario poiché non dipendo da alcun metadata. (La produzione normale di un binario statico spogliato rende eseguibile un ELF a 340 byte.)
È possibile ricavare una funzione da questo codice che è possibile chiamare da C. Il salvataggio e il ripristino del puntatore dello stack (forse in un registro MMX) e qualche altro overhead costerebbe qualche byte, ma anche salvare i byte ritornando con la stringa in memoria, invece di effettuare una write(1,buf,len)
chiamata di sistema. Penso che giocare a golf nel codice della macchina dovrebbe farmi perdere un po 'di tempo qui, dal momento che nessun altro ha nemmeno pubblicato una risposta in nessuna lingua senza precisione estesa nativa, ma penso che una versione di funzione di questo dovrebbe essere ancora sotto 120 byte senza ricominciare a giocare a golf cosa.
Algoritmo:
forza bruta a+=b; swap(a,b)
, troncando secondo necessità per mantenere solo le cifre decimali iniziali> = 1017. Funziona in 1min13s sul mio computer (o 322,47 miliardi di cicli di clock + - 0,05%) (e potrebbe essere un po 'più veloce con qualche byte in più di dimensione del codice, o fino a 62s con una dimensione del codice molto più grande dallo srotolamento del loop. No matematica intelligente, sto facendo lo stesso lavoro con meno spese generali). Si basa sull'implementazione Python di @ AndersKaseorg , che funziona in 12min35s sul mio computer (Skylake i7-6700k a 4,4 GHz). Nessuna versione ha alcun errore nella cache L1D, quindi il mio DDR4-2666 non ha importanza.
A differenza di Python, memorizzo i numeri di precisione estesa in un formato che rende libero il troncamento delle cifre decimali . Memorizzo gruppi di 9 cifre decimali per intero a 32 bit, quindi un offset del puntatore scarta le 9 cifre basse. Questo è in realtà 1 miliardo di base, che è una potenza di 10. (È pura coincidenza che questa sfida abbia bisogno del miliardesimo numero di Fibonacci, ma mi fa risparmiare un paio di byte contro due costanti separate.)
Seguendo la terminologia GMP , ogni blocco a 32 bit di un numero a precisione estesa viene chiamato "arto". L'esecuzione durante l'aggiunta deve essere generata manualmente con un confronto rispetto a 1e9, ma viene quindi utilizzata normalmente come input per le normali ADC
istruzioni dell'arto successivo. (Devo anche avvolgere manualmente l' [0..999999999]
intervallo, piuttosto che a 2 ^ 32 ~ = 4.295e9. Lo faccio senza diramazioni con lea
+ cmov
, usando il risultato del confronto dal confronto.)
Quando l'ultimo arto produce un risultato diverso da zero, le successive due iterazioni del loop esterno vengono lette da 1 arto più in alto del normale, ma continuano a scrivere nello stesso punto. Questo è come fare uno memcpy(a, a+4, 114*4)
spostamento a destra di 1 arto, ma fatto come parte dei successivi due cicli di addizione. Questo accade ogni ~ 18 iterazioni.
Hacks per il risparmio di dimensioni e prestazioni:
Le solite cose come lea ebx, [eax-4 + 1]
invece di mov ebx, 1
, quando lo so eax=4
. E l'utilizzo loop
in luoghi in cui LOOP
la lentezza ha solo un impatto minimo.
Troncare gratuitamente di 1 arto compensando i puntatori da cui leggiamo, mentre si scrive ancora all'inizio del buffer nel adc
loop interno. Leggiamo [edi+edx]
e scriviamo a [edi]
. Quindi possiamo ottenere edx=0
o 4
ottenere un offset di lettura-scrittura per la destinazione. Dobbiamo farlo per 2 iterazioni successive, prima compensando entrambe, poi solo compensando la dst. Rileviamo il secondo caso osservando esp&4
prima di reimpostare i puntatori nella parte anteriore dei buffer (utilizzando &= -1024
, poiché i buffer sono allineati). Vedi commenti nel codice.
L'ambiente di avvio del processo Linux (per un eseguibile statico) azzera la maggior parte dei registri e la memoria dello stack sotto esp
/ rsp
viene azzerata. Il mio programma ne approfitta. In una versione con funzione callable di questo (dove lo stack non allocato potrebbe essere sporco), potrei usare BSS per memoria azzerata (al costo di forse altri 4 byte per impostare i puntatori). L'azzeramento edx
richiederebbe 2 byte. L'ABI x86-64 System V non garantisce nessuno di questi, ma l'implementazione di Linux ne fa zero (per evitare perdite di informazioni dal kernel). In un processo collegato dinamicamente, /lib/ld.so
viene eseguito prima _start
e lascia registri diversi da zero (e probabilmente immondizia in memoria sotto il puntatore dello stack).
Continuo -1024
a ebx
usarlo al di fuori dei loop. Utilizzare bl
come contatore per i loop interni, che termina con zero (che è il byte basso di -1024
, ripristinando così la costante per l'uso all'esterno del loop). Intel Haswell e versioni successive non hanno penalità di fusione dei registri parziali per i registri low8 (e in effetti non li rinominano nemmeno separatamente) , quindi c'è una dipendenza dal registro completo, come su AMD (non è un problema qui). Ciò sarebbe orribile su Nehalem e precedenti, tuttavia, che hanno stalle a registro parziale durante la fusione. Ci sono altri posti in cui scrivo registri parziali e poi leggo il reg completo senza xor
-zeroing o amovzx
, di solito perché so che qualche codice precedente ha azzerato i byte superiori, e di nuovo va bene su AMD e Intel SnB-family, ma lento su Intel pre-Sandybridge.
Uso 1024
il numero di byte per scrivere su stdout ( sub edx, ebx
), quindi il mio programma stampa alcuni byte di immondizia dopo le cifre di Fibonacci, perché mov edx, 1000
costa più byte.
(non utilizzato) adc ebx,ebx
con EBX = 0 per ottenere EBX = CF, risparmiando 1 byte vs. setc bl
.
dec
/ jnz
all'interno di un adc
loop conserva CF senza causare uno stallo a flag parziale quando adc
legge flag su Intel Sandybridge e versioni successive. È negativo per le CPU precedenti , ma AFAIK è gratuito su Skylake. O nel peggiore dei casi, un extra in più.
Usa la memoria sotto esp
come una gigantesca zona rossa . Poiché si tratta di un programma Linux completo, so di non aver installato alcun gestore di segnale e che nient'altro ostruirà in modo asincrono la memoria dello stack dello spazio utente. Questo potrebbe non essere il caso di altri sistemi operativi.
Approfitta dello stack-engine per risparmiare la massima larghezza di banda utilizzando pop eax
(1 uop + occasionali stack-sync uop) anziché lodsd
(2 uops su Haswell / Skylake, 3 su IvB e precedenti secondo le tabelle di istruzioni di Agner Fog )). IIRC, questo ha ridotto il tempo di esecuzione da circa 83 secondi a 73. Probabilmente avrei potuto ottenere la stessa velocità dall'uso di un mov
con una modalità di indirizzamento indicizzato, come mov eax, [edi+ebp]
dove ebp
tiene l'offset tra i buffer src e dst. (Renderebbe il codice esterno al ciclo interno più complesso, dovendo negare il registro offset come parte dello scambio di src e dst per le iterazioni di Fibonacci.) Vedi la sezione "performance" di seguito.
avviare la sequenza assegnando alla prima iterazione un carry-in (un byte stc
), anziché archiviare un 1
in memoria ovunque. Molte altre cose specifiche del problema documentate nei commenti.
Elenco NASM (codice macchina + sorgente) , generato con nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Quindi ho rimosso a mano alcuni blocchi di elementi commentati, quindi la numerazione delle righe presenta delle lacune.) Per eliminare le colonne iniziali in modo da poterle inserire in YASM o NASM, utilizzare cut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.
1 machine global _start
2 code _start:
3 address
4 00000000 B900CA9A3B mov ecx, 1000000000 ; Fib(ecx) loop counter
5 ; lea ebp, [ecx-1] ; base-1 in the base(pointer) register ;)
6 00000005 89CD mov ebp, ecx ; not wrapping on limb==1000000000 doesn't change the result.
7 ; It's either self-correcting after the next add, or shifted out the bottom faster than Fib() grows.
8
42
43 ; mov esp, buf1
44
45 ; mov esi, buf1 ; ungolfed: static buffers instead of the stack
46 ; mov edi, buf2
47 00000007 BB00FCFFFF mov ebx, -1024
48 0000000C 21DC and esp, ebx ; alignment necessary for convenient pointer-reset
49 ; sar ebx, 1
50 0000000E 01DC add esp, ebx ; lea edi, [esp + ebx]. Can't skip this: ASLR or large environment can put ESP near the bottom of a 1024-byte block to start with
51 00000010 8D3C1C lea edi, [esp + ebx*1]
52 ;xchg esp, edi ; This is slightly faster. IDK why.
53
54 ; It's ok for EDI to be below ESP by multiple 4k pages. On Linux, IIRC the main stack automatically extends up to ulimit -s, even if you haven't adjusted ESP. (Earlier I used -4096 instead of -1024)
55 ; After an even number of swaps, EDI will be pointing to the lower-addressed buffer
56 ; This allows a small buffer size without having the string step on the number.
57
58 ; registers that are zero at process startup, which we depend on:
59 ; xor edx, edx
60 ;; we also depend on memory far below initial ESP being zeroed.
61
62 00000013 F9 stc ; starting conditions: both buffers zeroed, but carry-in = 1
63 ; starting Fib(0,1)->0,1,1,2,3 vs. Fib(1,0)->1,0,1,1,2 starting "backwards" puts us 1 count behind
66
67 ;;; register usage:
68 ;;; eax, esi: scratch for the adc inner loop, and outer loop
69 ;;; ebx: -1024. Low byte is used as the inner-loop limb counter (ending at zero, restoring the low byte of -1024)
70 ;;; ecx: outer-loop Fibonacci iteration counter
71 ;;; edx: dst read-write offset (for "right shifting" to discard the least-significant limb)
72 ;;; edi: dst pointer
73 ;;; esp: src pointer
74 ;;; ebp: base-1 = 999999999. Actually still happens to work with ebp=1000000000.
75
76 .fibonacci:
77 limbcount equ 114 ; 112 = 1006 decimal digits / 9 digits per limb. Not enough for 1000 correct digits, but 114 is.
78 ; 113 would be enough, but we depend on limbcount being even to avoid a sub
79 00000014 B372 mov bl, limbcount
80 .digits_add:
81 ;lodsd ; Skylake: 2 uops. Or pop rax with rsp instead of rsi
82 ; mov eax, [esp]
83 ; lea esp, [esp+4] ; adjust ESP without affecting CF. Alternative, load relative to edi and negate an offset? Or add esp,4 after adc before cmp
84 00000016 58 pop eax
85 00000017 130417 adc eax, [edi + edx*1] ; read from a potentially-offset location (but still store to the front)
86 ;; jz .out ;; Nope, a zero digit in the result doesn't mean the end! (Although it might in base 10**9 for this problem)
87
88 %if 0 ;; slower version
;; could be even smaller (and 5.3x slower) with a branch on CF: 25% mispredict rate
89 mov esi, eax
90 sub eax, ebp ; 1000000000 ; sets CF opposite what we need for next iteration
91 cmovc eax, esi
92 cmc ; 1 extra cycle of latency for the loop-carried dependency. 38,075Mc for 100M iters (with stosd).
93 ; not much worse: the 2c version bottlenecks on the front-end bottleneck
94 %else ;; faster version
95 0000001A 8DB0003665C4 lea esi, [eax - 1000000000]
96 00000020 39C5 cmp ebp, eax ; sets CF when (base-1) < eax. i.e. when eax>=base
97 00000022 0F42C6 cmovc eax, esi ; eax %= base, keeping it in the [0..base) range
98 %endif
99
100 %if 1
101 00000025 AB stosd ; Skylake: 3 uops. Like add + non-micro-fused store. 32,909Mcycles for 100M iters (with lea/cmp, not sub/cmc)
102 %else
103 mov [edi], eax ; 31,954Mcycles for 100M iters: faster than STOSD
104 lea edi, [edi+4] ; Replacing this with ADD EDI,4 before the CMP is much slower: 35,083Mcycles for 100M iters
105 %endif
106
107 00000026 FECB dec bl ; preserves CF. The resulting partial-flag merge on ADC would be slow on pre-SnB CPUs
108 00000028 75EC jnz .digits_add
109 ; bl=0, ebx=-1024
110 ; esi has its high bit set opposite to CF
111 .end_innerloop:
112 ;; after a non-zero carry-out (CF=1): right-shift both buffers by 1 limb, over the course of the next two iterations
113 ;; next iteration with r8 = 1 and rsi+=4: read offset from both, write normal. ends with CF=0
114 ;; following iter with r8 = 1 and rsi+=0: read offset from dest, write normal. ends with CF=0
115 ;; following iter with r8 = 0 and rsi+=0: i.e. back to normal, until next carry-out (possible a few iters later)
116
117 ;; rdi = bufX + 4*limbcount
118 ;; rsi = bufY + 4*limbcount + 4*carry_last_time
119
120 ; setc [rdi]
123 0000002A 0F92C2 setc dl
124 0000002D 8917 mov [edi], edx ; store the carry-out into an extra limb beyond limbcount
125 0000002F C1E202 shl edx, 2
139 ; keep -1024 in ebx. Using bl for the limb counter leaves bl zero here, so it's back to -1024 (or -2048 or whatever)
142 00000032 89E0 mov eax, esp ; test/setnz could work, but only saves a byte if we can somehow avoid the or dl,al
143 00000034 2404 and al, 4 ; only works if limbcount is even, otherwise we'd need to subtract limbcount first.
148 00000036 87FC xchg edi, esp ; Fibonacci: dst and src swap
149 00000038 21DC and esp, ebx ; -1024 ; revert to start of buffer, regardless of offset
150 0000003A 21DF and edi, ebx ; -1024
151
152 0000003C 01D4 add esp, edx ; read offset in src
155 ;; after adjusting src, so this only affects read-offset in the dst, not src.
156 0000003E 08C2 or dl, al ; also set r8d if we had a source offset last time, to handle the 2nd buffer
157 ;; clears CF for next iter
165 00000040 E2D2 loop .fibonacci ; Maybe 0.01% slower than dec/jnz overall
169 to_string:
175 stringdigits equ 9*limbcount ; + 18
176 ;;; edi and esp are pointing to the start of buffers, esp to the one most recently written
177 ;;; edi = esp +/- 2048, which is far enough away even in the worst case where they're growing towards each other
178 ;;; update: only 1024 apart, so this only works for even iteration-counts, to prevent overlap
180 ; ecx = 0 from the end of the fib loop
181 ;and ebp, 10 ; works because the low byte of 999999999 is 0xff
182 00000042 8D690A lea ebp, [ecx+10] ;mov ebp, 10
183 00000045 B172 mov cl, (stringdigits+8)/9
184 .toascii: ; slow but only used once, so we don't need a multiplicative inverse to speed up div by 10
185 ;add eax, [rsi] ; eax has the carry from last limb: 0..3 (base 4 * 10**9)
186 00000047 58 pop eax ; lodsd
187 00000048 B309 mov bl, 9
188 .toascii_digit:
189 0000004A 99 cdq ; edx=0 because eax can't have the high bit set
190 0000004B F7F5 div ebp ; edx=remainder = low digit = 0..9. eax/=10
197 0000004D 80C230 add dl, '0'
198 ; stosb ; clobber [rdi], then inc rdi
199 00000050 4F dec edi ; store digits in MSD-first printing order, working backwards from the end of the string
200 00000051 8817 mov [edi], dl
201
202 00000053 FECB dec bl
203 00000055 75F3 jnz .toascii_digit
204
205 00000057 E2EE loop .toascii
206
207 ; Upper bytes of eax=0 here. Also AL I think, but that isn't useful
208 ; ebx = -1024
209 00000059 29DA sub edx, ebx ; edx = 1024 + 0..9 (leading digit). +0 in the Fib(10**9) case
210
211 0000005B B004 mov al, 4 ; SYS_write
212 0000005D 8D58FD lea ebx, [eax-4 + 1] ; fd=1
213 ;mov ecx, edi ; buf
214 00000060 8D4F01 lea ecx, [edi+1] ; Hard-code for Fib(10**9), which has one leading zero in the highest limb.
215 ; shr edx, 1 ; for use with edx=2048
216 ; mov edx, 100
217 ; mov byte [ecx+edx-1], 0xa;'\n' ; count+=1 for newline
218 00000063 CD80 int 0x80 ; write(1, buf+1, 1024)
219
220 00000065 89D8 mov eax, ebx ; SYS_exit=1
221 00000067 CD80 int 0x80 ; exit(ebx=1)
222
# next byte is 0x69, so size = 0x69 = 105 bytes
Probabilmente c'è spazio per giocare a golf un po 'più di byte, ma ho già trascorso almeno 12 ore su questo in 2 giorni. Non voglio sacrificare la velocità, anche se è molto più che abbastanza veloce e c'è spazio per ridurla in modi che costano la velocità . Parte della mia ragione per la pubblicazione è mostrare quanto velocemente posso fare una versione asm di forza bruta. Se qualcuno vuole davvero andare per dimensioni minime ma forse 10 volte più lento (ad esempio 1 cifra per byte), sentiti libero di copiarlo come punto di partenza.
L'eseguibile risultante (da yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) è 340B (eliminato):
size fibonacci-1G
text data bss dec hex filename
105 0 0 105 69 fibonacci-1G
Prestazione
Il adc
ciclo interno è di 10 uops di dominio fuso su Skylake (+1 stack-sync ogni ~ 128 byte), quindi può emettere uno per ~ 2,5 cicli su Skylake con throughput front-end ottimale (ignorando gli stack-sync uops) . La latenza del percorso critico è di 2 cicli, per la catena di dipendenze trasportata in loop dall'iterazione successiva adc
-> cmp
-> adc
, quindi il collo di bottiglia dovrebbe essere il limite di emissione front-end di ~ 2,5 cicli per iterazione.
adc eax, [edi + edx]
è 2 uops di dominio non utilizzati per le porte di esecuzione: load + ALU. Si micro-fonde nei decodificatori (1 dominio di tipo fuso), ma non laminati nella fase di emissione a 2 circuiti di dominio fuso, a causa della modalità di indirizzamento indicizzato, anche su Haswell / Skylake . Ho pensato che sarebbe rimasto micro-fuso, come add eax, [edi + edx]
fa, ma forse mantenendo le modalità di indirizzamento indicizzato micro-fuso non funziona per gli utenti che hanno già 3 ingressi (flag, memoria e destinazione). Quando l'ho scritto, pensavo che non avrebbe avuto un rovescio della performance, ma mi sbagliavo. Questo modo di gestire il troncamento rallenta ogni volta il ciclo interno, sia che edx
sia 0 o 4.
Sarebbe più veloce gestire l'offset di lettura-scrittura per il dst compensando edi
e usando edx
per regolare il negozio. Quindi adc eax, [edi]
/ ... / mov [edi+edx], eax
/ lea edi, [edi+4]
invece di stosd
. Haswell e versioni successive possono mantenere un negozio indicizzato microfuso. (Anche Sandybridge / IvB lo svelerebbe.)
Su Intel Haswell e precedenti, adc
e cmovc
sono 2 uops ciascuno, con latenza 2c . (non adc eax, [edi+edx]
è ancora laminato su Haswell, ed emette 3 uops di dominio fuso). Broadwell e successivi consentono uops a 3 input per più di un semplice FMA (Haswell), rendendo adc
e cmovc
(e un paio di altre cose) istruzioni single-uop, come se fossero su AMD da molto tempo. (Questo è uno dei motivi per cui AMD ha fatto bene nei benchmark GMP di precisione estesa per lungo tempo.) Comunque, il ciclo interno di Haswell dovrebbe essere di 12 uops (+1 stack-sync di tanto in tanto occasionalmente), con un collo di bottiglia del front-end di ~ 3c per iter nel migliore dei casi, ignorando uops di sincronizzazione dello stack.
L'uso pop
senza bilanciamento push
all'interno di un loop significa che il loop non può essere eseguito dall'LSD (loop stream detector) e deve essere riletto dalla cache uop nell'IDQ ogni volta. Semmai, è una buona cosa su Skylake, dal momento che un ciclo di 9 o 10 uop non emette in modo ottimale a 4 uop ad ogni ciclo . Questo è probabilmente parte del motivo per cui la sostituzione lodsd
con ha pop
aiutato tanto. (L'LSD non può bloccare gli uops perché ciò non lascerebbe spazio per inserire uno stack-sync uop .) (A proposito, un aggiornamento del microcodice disabilita completamente l'LSD su Skylake e Skylake-X per correggere un errore. Ho misurato il sopra prima di ottenere quell'aggiornamento.)
L'ho profilato su Haswell e ho scoperto che funziona con 381,31 miliardi di cicli di clock (indipendentemente dalla frequenza della CPU, poiché utilizza solo cache L1D, non memoria). La velocità di emissione del front-end è stata di 3,72 uops di dominio fuso per clock, rispetto a 3,70 per Skylake. (Ma ovviamente le istruzioni per ciclo erano scese a 2,42 da 2,87, perché adc
e cmov
sono 2 uops su Haswell.)
push
sostituire stosd
probabilmente non sarebbe di grande aiuto, perché adc [esp + edx]
ogni volta si innescerebbe uno stack-sync. E costerebbe un byte per std
così lodsd
va nella direzione opposta. ( mov [edi], eax
/ lea edi, [edi+4]
sostituire stosd
è una vittoria, passando da 32.909 cicli per 100 milioni di iter a 31.954 cicli per 100 milioni di iter. Sembra che stosd
decodifichi come 3 uops, con gli indirizzi di negozio / dati di negozio non micro-fusi, quindi push
+ stack-sync uops potrebbe essere ancora più veloce di stosd
)
Le prestazioni effettive di ~ 322,47 miliardi di cicli per iterazioni 1G di 114 arti raggiungono i 2.824 cicli per iterazione del loop interno , per la veloce versione 105B su Skylake. (Vedi l' ocperf.py
output di seguito). È più lento di quanto avevo previsto dall'analisi statica, ma stavo ignorando il sovraccarico del ciclo esterno e tutti i cicli di sincronizzazione dello stack.
Perf contrasta branches
e branch-misses
mostra che il ciclo interno fornisce una previsione errata una volta per ciclo esterno (nell'ultima iterazione, quando non viene preso). Ciò rappresenta anche una parte dei tempi supplementari.
Ho potuto risparmiare codice-size rendendo più interna anello una latenza 3 cicli per il percorso critico, utilizzando mov esi,eax
/ sub eax,ebp
/ cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) anziché lea esi, [eax - 1000000000]
/ cmp ebp,eax
/ cmovc
(6 + 2 + 3 = 11B ). Il cmov
/ stosd
è fuori dal percorso critico. (L'editor incrementale di stosd
può essere eseguito separatamente dall'archivio, quindi ogni iterazione si stacca da una breve catena di dipendenze.) Ha usato per salvare un altro 1B modificando l'istruzione init di ebp da lea ebp, [ecx-1]
a mov ebp,eax
, ma ho scoperto che avere l'erroreebp
non ha cambiato il risultato. Ciò consentirebbe a un arto di essere esattamente == 1000000000 invece di avvolgere e produrre un carry, ma questo errore si propaga più lentamente della crescita di Fib (), quindi ciò non modifica le cifre 1k iniziali del risultato finale. Inoltre, penso che l'errore possa correggersi quando stiamo solo aggiungendo, poiché c'è spazio in un arto per trattenerlo senza trabocco. Perfino 1G + 1G non trabocca di un numero intero a 32 bit, quindi alla fine percolerà verso l'alto o verrà troncato.
La versione di latenza 3c è 1 extra in più, quindi il front-end può emetterlo a uno per 2,75c cicli su Skylake, solo leggermente più veloce del back-end può eseguirlo. (Su Haswell, sarà 13 uops in totale poiché utilizza ancora adc
e cmov
, e collo di bottiglia sul front-end a 3,25 c per iter).
In pratica corre un fattore di 1,18 più lento su Skylake (3,34 cicli per arto), piuttosto che 3 / 2,5 = 1,2 che avevo predetto per sostituire il collo di bottiglia del front-end con il collo di bottiglia della latenza dal solo guardare il ciclo interno senza stack-sync UOP. Dato che lo stack-sync uops danneggia solo la versione veloce (collo di bottiglia sul front-end anziché latenza), non ci vuole molto per spiegarlo. ad es. 3 / 2.54 = 1.18.
Un altro fattore è che la versione di latenza 3c può rilevare il colpevole di lasciare il ciclo interno mentre il percorso critico è ancora in esecuzione (perché il front-end può andare avanti rispetto al back-end, lasciando che l'esecuzione fuori servizio esegua il loop- contromisure), quindi la penalità per errore effettivo è inferiore. Perdere quei cicli front-end consente al back-end di recuperare.
Se non fosse per quello, potremmo forse accelerare la cmc
versione 3c usando un ramo nel ciclo esterno invece della gestione senza rami degli offset carry_out -> edx ed esp. Branch-prediction + esecuzione speculativa per una dipendenza di controllo anziché una dipendenza di dati potrebbe consentire alla successiva iterazione di iniziare a eseguire il adc
ciclo mentre i loop del precedente ciclo interno erano ancora in volo. Nella versione senza rami, gli indirizzi di carico nel loop interno hanno una dipendenza dati da CF dall'ultimo adc
dell'ultimo arto.
Il collo di bottiglia della versione a ciclo interno di latenza 2c è presente sul front-end, quindi il back-end praticamente mantiene il passo. Se il codice del ciclo esterno fosse ad alta latenza, il front-end potrebbe andare avanti emettendo uops dalla successiva iterazione del ciclo interno. (Ma in questo caso le cose del ciclo esterno hanno un sacco di ILP e nessuna roba ad alta latenza, quindi il back-end non ha molto da recuperare quando inizia a masticare attraverso il ciclo nello schedulatore fuori servizio come i loro input diventano pronti).
### Output from a profiled run
$ asm-link -m32 fibonacci-1G.asm && (size fibonacci-1G; echo disas fibonacci-1G) && ocperf.py stat -etask-clock,context-switches:u,cpu-migrations:u,page-faults:u,cycles,instructions,uops_issued.any,uops_executed.thread,uops_executed.stall_cycles -r4 ./fibonacci-1G
+ yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm
+ ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
text data bss dec hex filename
106 0 0 106 6a fibonacci-1G
disas fibonacci-1G
perf stat -etask-clock,context-switches:u,cpu-migrations:u,page-faults:u,cycles,instructions,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/,cpu/event=0xb1,umask=0x1,inv=1,cmask=1,name=uops_executed_stall_cycles/ -r4 ./fibonacci-1G
79523178745546834678293851961971481892555421852343989134530399373432466861825193700509996261365567793324820357232224512262917144562756482594995306121113012554998796395160534597890187005674399468448430345998024199240437534019501148301072342650378414269803983873607842842319964573407827842007677609077777031831857446565362535115028517159633510239906992325954713226703655064824359665868860486271597169163514487885274274355081139091679639073803982428480339801102763705442642850327443647811984518254621305295296333398134831057713701281118511282471363114142083189838025269079177870948022177508596851163638833748474280367371478820799566888075091583722494514375193201625820020005307983098872612570282019075093705542329311070849768547158335856239104506794491200115647629256491445095319046849844170025120865040207790125013561778741996050855583171909053951344689194433130268248133632341904943755992625530254665288381226394336004838495350706477119867692795685487968552076848977417717843758594964253843558791057997424878788358402439890396,�X\�;3�I;ro~.�'��R!q��%��X'B �� 8w��▒Ǫ�
... repeated 3 more times, for the 3 more runs we're averaging over
Note the trailing garbage after the trailing digits.
Performance counter stats for './fibonacci-1G' (4 runs):
73438.538349 task-clock:u (msec) # 1.000 CPUs utilized ( +- 0.05% )
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
2 page-faults:u # 0.000 K/sec ( +- 11.55% )
322,467,902,120 cycles:u # 4.391 GHz ( +- 0.05% )
924,000,029,608 instructions:u # 2.87 insn per cycle ( +- 0.00% )
1,191,553,612,474 uops_issued_any:u # 16225.181 M/sec ( +- 0.00% )
1,173,953,974,712 uops_executed_thread:u # 15985.530 M/sec ( +- 0.00% )
6,011,337,533 uops_executed_stall_cycles:u # 81.855 M/sec ( +- 1.27% )
73.436831004 seconds time elapsed ( +- 0.05% )
( +- x %)
è la deviazione standard sulle 4 corse per quel conteggio. Interessante che esegua un numero così tondo di istruzioni. Quel 924 miliardi non è una coincidenza. Suppongo che il ciclo esterno esegua un totale di 924 istruzioni.
uops_issued
è un conteggio di domini fusi (rilevante per la larghezza di banda dei problemi di front-end), mentre uops_executed
è un conteggio di domini non fusi (numero di uops inviati alle porte di esecuzione). La micro-fusione racchiude 2 Uops di dominio non fuso in un solo UOP di dominio fuso, ma l' eliminazione di mov significa che alcuni UOP di dominio fuso non necessitano di porte di esecuzione. Vedi la domanda collegata per ulteriori informazioni sul conteggio dei domini uops e fused vs. unfused. (Vedi anche le tabelle di istruzioni di Agner Fog e la guida di Uarch e altri link utili nel wiki SO x86 del tag ).
Da un'altra corsa, misurando cose diverse: i mancati cache L1D sono totalmente insignificanti, come previsto per la lettura / scrittura degli stessi due buffer 456B. Il ramo del ciclo interno indica erroneamente una volta per ciclo esterno (quando non viene preso per lasciare il ciclo). (Il tempo totale è maggiore perché il computer non era completamente inattivo. Probabilmente l'altro core logico era attivo per un po 'di tempo e più tempo è stato impiegato in interruzioni (poiché la frequenza misurata dallo spazio utente era più lontana sotto i 4.400 GHz). O più core erano attivi per la maggior parte del tempo, riducendo il turbo massimo. Non ho tracciato cpu_clk_unhalted.one_thread_active
per vedere se la competizione HT fosse un problema.)
### Another run of the same 105/106B "main" version to check other perf counters
74510.119941 task-clock:u (msec) # 1.000 CPUs utilized
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
2 page-faults:u # 0.000 K/sec
324,455,912,026 cycles:u # 4.355 GHz
924,000,036,632 instructions:u # 2.85 insn per cycle
228,005,015,542 L1-dcache-loads:u # 3069.535 M/sec
277,081 L1-dcache-load-misses:u # 0.00% of all L1-dcache hits
0 ld_blocks_partial_address_alias:u # 0.000 K/sec
115,000,030,234 branches:u # 1543.415 M/sec
1,000,017,804 branch-misses:u # 0.87% of all branches
Il mio codice potrebbe essere eseguito in meno cicli su Ryzen, che può emettere 5 uops per ciclo (o 6 quando alcuni di essi sono istruzioni 2-uop, come roba AVX 256b su Ryzen). Non sono sicuro di cosa farebbe il suo front-end stosd
, ovvero 3 uops su Ryzen (uguale a Intel). Penso che le altre istruzioni nel ciclo interno abbiano la stessa latenza di Skylake e tutte le single-uop. (Compreso adc eax, [edi+edx]
, che è un vantaggio rispetto a Skylake).
Questo potrebbe probabilmente essere significativamente più piccolo, ma forse 9 volte più lento, se memorizzassi i numeri come 1 cifra decimale per byte . Generare il carry-out cmp
e adattarsi con cmov
funzionerebbe allo stesso modo, ma farebbe 1/9 del lavoro. Funzionerebbero anche 2 cifre decimali per byte (base-100, non BCD a 4 bit con un lentoDAA
) e div r8
/ / add ax, 0x3030
trasforma uno 0-99 byte in due cifre ASCII in ordine di stampa. Ma 1 cifra per byte non è affatto necessaria div
, basta un loop e l'aggiunta di 0x30. Se conservo i byte in ordine di stampa, ciò renderebbe il 2 ° ciclo davvero semplice.
L'uso di 18 o 19 cifre decimali per numero intero a 64 bit (in modalità 64 bit) lo farebbe funzionare circa due volte più veloce, ma costerebbe dimensioni significative del codice per tutti i prefissi REX e per le costanti a 64 bit. Arti a 32 bit in modalità 64 bit ne impediscono l'utilizzo pop eax
anziché lodsd
. Potrei ancora evitare i prefissi REX usando esp
come un registro scratch senza puntatore (scambiando l'uso di esi
e esp
), invece di usare r8d
come un ottavo registro.
Se si effettua una versione con funzione richiamabile, la conversione in 64 bit e l'utilizzo r8d
potrebbero essere più economici del salvataggio / ripristino rsp
. Inoltre, a 64 bit non è possibile utilizzare la dec r32
codifica a un byte (poiché si tratta di un prefisso REX). Ma per lo più ho finito con l'utilizzo di dec bl
2 byte. (Perché ho una costante nei byte superiori di ebx
e la uso solo al di fuori dei loop interni, il che funziona perché il byte basso della costante è 0x00
.)
Versione ad alte prestazioni
Per ottenere le massime prestazioni (non il golf di codice), vorrai srotolare il ciclo interno in modo che esegua al massimo 22 iterazioni, che è un modello abbastanza breve preso / non preso per i predittori di ramo per fare bene. Nei miei esperimenti, mov cl, 22
prima che un .inner: dec cl/jnz .inner
loop abbia pochissimi errori (come 0,05%, molto meno di uno per ciclo completo del loop interno), ma mov cl,23
fraintendimenti da 0,35 a 0,6 volte per loop interno. 46
è particolarmente male, prevedendo erroneamente ~ 1,28 volte per loop interno (128M volte per iterazioni di loop esterno 100M). 114
erroneamente previsto una volta per ciclo interno, lo stesso che ho trovato come parte del ciclo di Fibonacci.
Mi sono incuriosito e l'ho provato, srotolando il ciclo interno di 6 con un %rep 6
(perché questo divide equamente 114). Ciò ha eliminato per lo più i fallimenti delle filiali. L'ho reso edx
negativo e l' ho usato come offset per i mov
negozi, quindi adc eax,[edi]
potrei rimanere microfuso. (E così ho potuto evitare stosd
). Ho estratto il lea
per aggiornare edi
dal %rep
blocco, quindi esegue solo un aggiornamento del puntatore per 6 negozi.
Mi sono anche sbarazzato di tutte le cose del registro parziale nel loop esterno, anche se non penso che sia stato significativo. Potrebbe aver aiutato leggermente avere CF alla fine del loop esterno non dipendente dall'ADC finale, quindi alcuni dei loop del ciclo interno possono iniziare. Probabilmente il codice del loop esterno potrebbe essere ottimizzato un po 'di più, poiché è neg edx
stata l'ultima cosa che ho fatto, dopo averlo sostituito xchg
con solo 2 mov
istruzioni (dato che ne avevo ancora 1), e riordinare le catene dep insieme a far cadere l'8-bit registra cose.
Questa è la fonte NASM del solo ciclo di Fibonacci. È un sostituto drop-in per quella sezione della versione originale.
;;;; Main loop, optimized for performance, not code-size
%assign unrollfac 6
mov bl, limbcount/unrollfac ; and at the end of the outer loop
align 32
.fibonacci:
limbcount equ 114 ; 112 = 1006 decimal digits / 9 digits per limb. Not enough for 1000 correct digits, but 114 is.
; 113 would be enough, but we depend on limbcount being even to avoid a sub
; align 8
.digits_add:
%assign i 0
%rep unrollfac
;lodsd ; Skylake: 2 uops. Or pop rax with rsp instead of rsi
; mov eax, [esp]
; lea esp, [esp+4] ; adjust ESP without affecting CF. Alternative, load relative to edi and negate an offset? Or add esp,4 after adc before cmp
pop eax
adc eax, [edi+i*4] ; read from a potentially-offset location (but still store to the front)
;; jz .out ;; Nope, a zero digit in the result doesn't mean the end! (Although it might in base 10**9 for this problem)
lea esi, [eax - 1000000000]
cmp ebp, eax ; sets CF when (base-1) < eax. i.e. when eax>=base
cmovc eax, esi ; eax %= base, keeping it in the [0..base) range
%if 0
stosd
%else
mov [edi+i*4+edx], eax
%endif
%assign i i+1
%endrep
lea edi, [edi+4*unrollfac]
dec bl ; preserves CF. The resulting partial-flag merge on ADC would be slow on pre-SnB CPUs
jnz .digits_add
; bl=0, ebx=-1024
; esi has its high bit set opposite to CF
.end_innerloop:
;; after a non-zero carry-out (CF=1): right-shift both buffers by 1 limb, over the course of the next two iterations
;; next iteration with r8 = 1 and rsi+=4: read offset from both, write normal. ends with CF=0
;; following iter with r8 = 1 and rsi+=0: read offset from dest, write normal. ends with CF=0
;; following iter with r8 = 0 and rsi+=0: i.e. back to normal, until next carry-out (possible a few iters later)
;; rdi = bufX + 4*limbcount
;; rsi = bufY + 4*limbcount + 4*carry_last_time
; setc [rdi]
; mov dl, dh ; edx=0. 2c latency on SKL, but DH has been ready for a long time
; adc edx,edx ; edx = CF. 1B shorter than setc dl, but requires edx=0 to start
setc al
movzx edx, al
mov [edi], edx ; store the carry-out into an extra limb beyond limbcount
shl edx, 2
;; Branching to handle the truncation would break the data-dependency (of pointers) on carry-out from this iteration
;; and let the next iteration start, but we bottleneck on the front-end (9 uops)
;; not the loop-carried dependency of the inner loop (2 cycles for adc->cmp -> flag input of adc next iter)
;; Since the pattern isn't perfectly regular, branch mispredicts would hurt us
; keep -1024 in ebx. Using bl for the limb counter leaves bl zero here, so it's back to -1024 (or -2048 or whatever)
mov eax, esp
and esp, 4 ; only works if limbcount is even, otherwise we'd need to subtract limbcount first.
and edi, ebx ; -1024 ; revert to start of buffer, regardless of offset
add edi, edx ; read offset in next iter's src
;; maybe or edi,edx / and edi, 4 | -1024? Still 2 uops for the same work
;; setc dil?
;; after adjusting src, so this only affects read-offset in the dst, not src.
or edx, esp ; also set r8d if we had a source offset last time, to handle the 2nd buffer
mov esp, edi
; xchg edi, esp ; Fibonacci: dst and src swap
and eax, ebx ; -1024
;; mov edi, eax
;; add edi, edx
lea edi, [eax+edx]
neg edx ; negated read-write offset used with store instead of load, so adc can micro-fuse
mov bl, limbcount/unrollfac
;; Last instruction must leave CF clear for next iter
; loop .fibonacci ; Maybe 0.01% slower than dec/jnz overall
; dec ecx
sub ecx, 1 ; clear any flag dependencies. No faster than dec, at least when CF doesn't depend on edx
jnz .fibonacci
Prestazione:
Performance counter stats for './fibonacci-1G-performance' (3 runs):
62280.632258 task-clock (msec) # 1.000 CPUs utilized ( +- 0.07% )
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
3 page-faults:u # 0.000 K/sec ( +- 12.50% )
273,146,159,432 cycles # 4.386 GHz ( +- 0.07% )
757,088,570,818 instructions # 2.77 insn per cycle ( +- 0.00% )
740,135,435,806 uops_issued_any # 11883.878 M/sec ( +- 0.00% )
966,140,990,513 uops_executed_thread # 15512.704 M/sec ( +- 0.00% )
75,953,944,528 resource_stalls_any # 1219.544 M/sec ( +- 0.23% )
741,572,966 idq_uops_not_delivered_core # 11.907 M/sec ( +- 54.22% )
62.279833889 seconds time elapsed ( +- 0.07% )
Questo è per lo stesso Fib (1G), producendo la stessa uscita in 62,3 secondi anziché 73 secondi. (273.146G cicli, contro 322.467G. Poiché tutto colpisce nella cache L1, i cicli di core clock sono davvero tutto ciò che dobbiamo guardare.)
Nota il uops_issued
conteggio totale molto più basso , ben al di sotto del uops_executed
conteggio. Ciò significa che molti di loro erano micro-fusi: 1 uop nel dominio fuso (problema / ROB), ma 2 uop nel dominio non fuso (scheduler / unità di esecuzione)). E che pochi sono stati eliminati nella fase di emissione / ridenominazione (come la mov
copia del registro o xor
-zeroing, che devono essere emessi ma non necessitano di un'unità di esecuzione). Gli uops eliminati sbilancerebbero il conteggio nell'altro modo.
branch-misses
scende a ~ 400k, da 1G, quindi lo srotolamento ha funzionato. resource_stalls.any
è significativo ora, il che significa che il front-end non è più il collo di bottiglia: invece il back-end si sta allontanando e limitando il front-end. idq_uops_not_delivered.core
conta solo i cicli in cui il front-end non ha erogato uops, ma il back-end non è stato bloccato. È bello e basso, indicando alcuni colli di bottiglia front-end.
Curiosità: la versione di Python impiega più della metà del tempo a dividere per 10 invece di aggiungere. (La sostituzione di a/=10
con lo a>>=64
accelera di oltre un fattore 2, ma modifica il risultato a causa del troncamento binario! = Troncamento decimale.)
La mia versione asm è ovviamente ottimizzata in modo specifico per questa dimensione del problema, con l'iterazione del ciclo che conta hard coded. Anche lo spostamento di un numero di precisione arbitraria lo copierà, ma la mia versione può semplicemente leggere da un offset per le successive due iterazioni per saltare anche quello.
Ho profilato la versione di Python (64-bit python2.7 su Arch Linux):
ocperf.py stat -etask-clock,context-switches:u,cpu-migrations:u,page-faults:u,cycles,instructions,uops_issued.any,uops_executed.thread,arith.divider_active,branches,branch-misses,L1-dcache-loads,L1-dcache-load-misses python2.7 ./fibonacci-1G.anders-brute-force.py
795231787455468346782938519619714818925554218523439891345303993734324668618251937005099962613655677933248203572322245122629171445627564825949953061211130125549987963951605345978901870056743994684484303459980241992404375340195011483010723426503784142698039838736078428423199645734078278420076776090777770318318574465653625351150285171596335102399069923259547132267036550648243596658688604862715971691635144878852742743550811390916796390738039824284803398011027637054426428503274436478119845182546213052952963333981348310577137012811185112824713631141420831898380252690791778709480221775085968511636388337484742803673714788207995668880750915837224945143751932016258200200053079830988726125702820190750937055423293110708497685471583358562391045067944912001156476292564914450953190468498441700251208650402077901250135617787419960508555831719090539513446891944331302682481336323419049437559926255302546652883812263943360048384953507064771198676927956854879685520768489774177178437585949642538435587910579974100118580
Performance counter stats for 'python2.7 ./fibonacci-1G.anders-brute-force.py':
755380.697069 task-clock:u (msec) # 1.000 CPUs utilized
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
793 page-faults:u # 0.001 K/sec
3,314,554,673,632 cycles:u # 4.388 GHz (55.56%)
4,850,161,993,949 instructions:u # 1.46 insn per cycle (66.67%)
6,741,894,323,711 uops_issued_any:u # 8925.161 M/sec (66.67%)
7,052,005,073,018 uops_executed_thread:u # 9335.697 M/sec (66.67%)
425,094,740,110 arith_divider_active:u # 562.756 M/sec (66.67%)
807,102,521,665 branches:u # 1068.471 M/sec (66.67%)
4,460,765,466 branch-misses:u # 0.55% of all branches (44.44%)
1,317,454,116,902 L1-dcache-loads:u # 1744.093 M/sec (44.44%)
36,822,513 L1-dcache-load-misses:u # 0.00% of all L1-dcache hits (44.44%)
755.355560032 seconds time elapsed
I numeri in (parentesi) indicano per quanto tempo è stato campionato il perf counter. Quando si osservano più contatori di quelli supportati da HW, perf ruota tra diversi contatori ed estrapolati. Va benissimo per un lungo periodo dello stesso compito.
Se avessi eseguito perf
dopo aver impostato sysctl kernel.perf_event_paranoid = 0
(o in esecuzione perf
come root), avrebbe misurato 4.400GHz
. cycles:u
non conta il tempo trascorso in interrupt (o chiamate di sistema), ma solo cicli spazio utente. Il mio desktop era quasi completamente inattivo, ma questo è tipico.
Your program must be fast enough for you to run it and verify its correctness.
che dire della memoria?