32-бітний машинний код x86 (з системними дзвінками Linux): 106 105 байт
changelog: збережений байт у швидкій версії, оскільки константа "один за одним" не змінює результат для Fib (1G).
Або 102 байти для 18% повільнішої (у Skylake) версії (використовуючи mov
/ sub
/ cmc
замість lea
/ cmp
у внутрішньому циклі, щоб генерувати виконання та обгортання 10**9
замість 2**32
). Або 101 байт для більш повільної версії ~ 5,3x із гілкою в обробці, що переноситься, у самому циклі. (Я виміряв 25,4% коефіцієнта неправильного прогнозування!)
Або 104/101 байт, якщо дозволений провідний нуль. (Потрібно 1 додатковий байт, щоб пропустити жорсткий код 1 цифру виводу, що саме потрібно для Fib (10 ** 9)).
На жаль, режим NASM TIO, схоже, ігнорує -felf32
у прапорах компілятора. Ось посилання все-таки з моїм повним вихідним кодом, з усім безладом експериментальних ідей у коментарях.
Це повна програма . Він друкує перші 1000 цифр Fib (10 ** 9), а потім додаткові цифри (останні кілька з яких неправильні), а потім кілька байтів сміття (не враховуючи новий рядок). Велика частина сміття не є ASCII, так що ви можете труби через cat -v
. Хоча це не порушує мій емулятор терміналу (KDE konsole
). "Байти сміття" зберігають Fib (999999999). У мене вже був -1024
реєстр, тому було дешевше надрукувати 1024 байти, ніж належного розміру.
Я рахую лише машинний код (розмір текстового сегмента мого статичного виконуваного файлу), а не пух, який робить його виконуваним ELF. ( Можливі дуже маленькі виконувані файли ELF , але я не хотів цим заважати). Виявилося, що використання пам'яті стека замість BSS виявилося коротшим, тому я можу виправдати, що не рахую нічого іншого у двійковому, оскільки я не залежую від жодних метаданих. (Нормальний спосіб виготовлення позбавленого статичного двійкового файлу робить виконуваним ELF 340 байт.)
Ви можете зробити функцію з цього коду, на який ви могли б зателефонувати з C. Кошто коштуватиме кілька байтів, щоб зберегти / відновити покажчик стека (можливо, в регістрі MMX) та деякі інші накладні витрати, але також зберегти байти, повернувшись із рядком в пам'яті, замість того, щоб робити write(1,buf,len)
системний дзвінок. Я думаю, що гольф у машинному коді повинен заробити мене тут трохи, оскільки ніхто інший навіть не опублікував відповіді будь-якою мовою без рідної розширеної точності, але я вважаю, що версія цієї функції все ж повинна бути менше 120 байт, не переробляючи цілого гольфу. річ.
Алгоритм:
груба сила a+=b; swap(a,b)
, обрізання за необхідності, щоб зберегти лише провідні> = 1017 десяткових цифр. Він працює на 1min13s на моєму комп’ютері (або 322,47 мільярда тактових циклів + - 0,05%) (і може бути на кілька відсотків швидше з кількома додатковими байтами розміру коду, або до 62-х з набагато більшим розміром коду від розмотування циклу. Ні. розумна математика, просто виконуючи ту саму роботу з меншими накладними витратами). Він заснований на реалізації Python @ AndersKaseorg , яка працює на 12 хвилинах 35х на моєму комп'ютері (4,4 ГГц Skylake i7-6700k). Жодна з версій не має жодного кешу L1D, тому мій DDR4-2666 не має значення.
На відміну від Python, я зберігаю номери з розширеною точністю у форматі, який робить обрізання десяткових цифр безкоштовним . Я зберігаю групи з 9 десяткових цифр на 32-бітне ціле число, тому зміщення покажчика відкидає низькі 9 цифр. Це фактично базовий 1 мільярд, що є потужністю 10. (Чистий збіг обставин, що для цього виклику потрібне 1-мільярдне число Фібоначчі, але це врятує мені пару байтів проти двох окремих констант.)
Дотримуючись термінології GMP , кожен 32-розрядний фрагмент числа з підвищеною точністю називається "кінцівкою". Виконання під час додавання потрібно генерувати вручну з порівнянням проти 1e9, але потім зазвичай використовується як вхід до звичайної ADC
інструкції для наступної кінцівки. (Я також повинен вручну завернути до [0..999999999]
діапазону, а не на 2 ^ 32 ~ = 4.295e9. Я це роблю без гілок з lea
+ cmov
, використовуючи результат виконання порівняння.)
Коли остання кінцівка створює ненульовий винос, наступні дві ітерації зовнішньої петлі зчитуються на 1 кінцівку вище, ніж зазвичай, але все одно записують на те саме місце. Це як би зробити memcpy(a, a+4, 114*4)
зсув правої частини на 1 кінцівку, але виконано як частину наступних двох циклів додавання. Це відбувається кожні ~ 18 ітерацій.
Хаки для економії розміру та продуктивності:
Звичайні речі, як lea ebx, [eax-4 + 1]
замість того mov ebx, 1
, коли я це знаю eax=4
. І використання loop
в місцях, де LOOP
повільність має лише крихітний вплив.
Обрізати на 1 кінцівку безкоштовно, зміщуючи покажчики, з яких ми читаємо, при цьому все ще записуючи до початку буфера у adc
внутрішньому циклі. Ми читаємо з [edi+edx]
, і пишемо в [edi]
. Таким чином, ми можемо отримати edx=0
або 4
отримати компенсацію читання-запису для місця призначення. Нам потрібно зробити це для двох послідовних ітерацій, спочатку компенсуючи обидві, потім лише компенсуючи даст. Ми виявляємо другий випадок, дивлячись esp&4
перед тим, як скинути покажчики на передню частину буферів (використовуючи &= -1024
, тому що буфери вирівняні). Дивіться коментарі в коді.
Середовище запуску процесів Linux (для статичного виконуваного файлу) більшість реєстрів, а стек-пам'ять нижче esp
/ rsp
нульова. Моя програма цим користується. У версії цієї функції, що вимагає дзвінка (де нерозподілений стек може бути забрудненим), я міг би використовувати BSS для нульової пам'яті (ціною, можливо, ще 4-х байтів, щоб встановити покажчики). Нулювання edx
зайняло б 2 байти. Система x86-64 System V ABI не гарантує жодного з них, але реалізація цього Linux робить нульовий (щоб уникнути витоку інформації з ядра). У динамічно пов'язаному процесі він /lib/ld.so
запускається раніше _start
і не залишає регістри ненульовими (і, ймовірно, сміття в пам'яті нижче вказівника стека).
Я тримаю -1024
в ebx
для використання зовні петель. Використовуйте bl
як лічильник для внутрішніх циклів, що закінчуються нулем (що є низьким байтом -1024
, тим самим відновлюючи константу для використання поза циклом). Intel Haswell і пізніше не мають часткових реєстрів для об'єднання покарань для низьких8 регістрів (а насправді навіть не перейменовують їх окремо) , тому існує залежність від повного реєстру, як у AMD (тут не проблема). Це було б жахливо для Негалема та більш ранніх, однак при злитті вони мають стоянки з частковою реєстрацією. Є й інші місця, де я пишу часткові регістри, а потім читаю повний xor
регістр без -zeroing або amovzx
, як правило, тому, що я знаю, що деякий попередній код обнуляв верхні байти, і знову це нормально для AMD та Intel SnB-сімейства, але повільно для Intel-попереднього Sandybridge.
Я використовую 1024
як кількість байтів для запису в stdout ( sub edx, ebx
), тому моя програма друкує кілька байтів сміття після цифр Фібоначчі, оскільки mov edx, 1000
коштує більше байтів.
(Не використовується) adc ebx,ebx
з EBX = 0 , щоб отримати EBX = CF, економлячи 1 байт проти setc bl
.
dec
/ jnz
всередині adc
циклу зберігає CF, не викликаючи часткового прапора під час adc
зчитування прапорів на Intel Sandybridge та пізніших версіях. Це погано на попередніх процесорах , але AFAIK безкоштовно на Skylake. Або, в гіршому випадку, додатковий взагалі.
Використовуйте пам'ять нижче esp
як гігантську червону зону . Оскільки це повна програма для Linux, я знаю, що я не встановив жодних обробників сигналів, і що більше нічого не буде асинхронно обробляти стек пам'яті користувача. Це може бути не в інших ОС.
Скористайтеся механізмом стека, щоб зберегти пропускну здатність взагалі, використовуючи pop eax
(1 взагалі + випадковий синхронізація стека взагалі) замість lodsd
(2 уп на Haswell / Skylake, 3 на IvB і раніше відповідно до таблиць інструкцій Agner Fog )). IIRC, це знизило час роботи з приблизно 83 секунд до 73. Я, мабуть, міг би отримати ту ж швидкість від використання a mov
з індексованим режимом адресації, як, наприклад, mov eax, [edi+ebp]
де ebp
проводиться зміщення між src і dst буферами. (Це зробило б код поза внутрішньою петлею більш складним, змусивши звести нанівець регістр зміщення як частину заміни src та dst для ітерацій Фібоначчі.) Докладніше див. У розділі "Продуктивність" нижче.
Почніть послідовність, надаючи першій ітерації перенесення (один байт stc
), а не зберігати 1
пам'ять у будь-якому місці. Багато інших матеріалів, що стосуються конкретних проблем, задокументовані в коментарях.
Перелік NASM (машинний код + джерело) , згенерований за допомогою nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Тоді я видалив декілька блоків коментованих матеріалів, тому нумерація рядків має прогалини.) Щоб викреслити провідні стовпці, щоб ви могли подати її в YASM або NASM, використовуйте 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
З цього, мабуть, є можливість пограти ще кілька байтів, але я вже витратив принаймні 12 годин на це протягом 2 днів. Я не хочу жертвувати швидкістю, навіть якщо це спосіб більш ніж швидкий і є можливість зробити його меншим способом, який витрачає швидкість . Частина моєї причини публікації повідомляє про те, наскільки швидко я можу зробити версию ASM грубої сили. Якщо хтось хоче дійсно отримати мінімальний розмір, але, можливо, на 10 разів повільніше (наприклад, 1 цифра на байт), сміливо скопіюйте це як вихідну точку.
Отриманий виконуваний файл (від yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) становить 340В (позбавлений):
size fibonacci-1G
text data bss dec hex filename
105 0 0 105 69 fibonacci-1G
Продуктивність
Внутрішній adc
цикл становить 10 Uops з плавленим доменом на Skylake (+1 стек-синхронізація взагалі кожні ~ 128 байт), тому він може видавати за один цикл на ~ 2,5 циклу на Skylake з оптимальною пропускною здатністю (ігноруючи стек-синхронізацію Uops) . Затримка критичного шляху становить 2 цикли, для adc
-> cmp
-> adc
ланцюга залежності наступної ітерації, що перебуває в циклі, тому вузьким місцем має бути гранична кількість випусків - 2,5 циклу за ітерацію.
adc eax, [edi + edx]
є 2 Uops з конденсованим доменом для портів виконання: load + ALU. Він мікроплавкі в декодерах (1 загальнодоступний домен взагалі), але не ламінований на етапі випуску до 2 Uops з плавленим доменом, через індексований режим адресації, навіть на Haswell / Skylake . Я думав, що він залишиться мікроплавким, як add eax, [edi + edx]
це робиться, але, можливо, зберігання режимів індексованої адреси мікро-злиття не працює для Uops, у яких вже є 3 входи (прапори, пам'ять та призначення). Коли я писав це, я думав, що це не матиме зниження продуктивності, але я помилявся. Цей спосіб поводження з усіченням щоразу сповільнює внутрішню петлю, будь edx
то 0 або 4.
Швидше було б обробити зсув читання-запису для dst шляхом компенсації edi
та за допомогою edx
налаштування магазину. Так adc eax, [edi]
/ ... / mov [edi+edx], eax
/ lea edi, [edi+4]
замість stosd
. Haswell і пізніше можуть тримати індексований магазин мікроплавким. (Сендібрідж / IvB також розпалить його.)
У Intel Haswell і раніше, adc
і cmovc
це 2 уопи кожен, із затримкою 2с . ( adc eax, [edi+edx]
досі не має ламінованого покриття на Haswell і видає 3 Uops з плавленим доменом) Бродвелл та пізніше дозволяють 3-вхідні Uops для більше, ніж просто FMA (Haswell), роблячи adc
та cmovc
(і ще пару речей) інструкції на одне ціле, як вони були на AMD вже давно. (Це одна з причин, що AMD вже давно успішно справляється з орієнтирами GMP з розширеною точністю.) У будь-якому випадку внутрішня петля Haswell повинна становити 12 уп (час від часу синхронізація +1 стека), з вузьким місцем - 3c на передній iter кращий випадок, ігноруючи стеки синхронізації стека.
Використання pop
без балансування push
всередині циклу означає, що цикл не може запускатися з LSD (детектор потокового циклу) , і його потрібно кожного разу перечитувати з загального кешу в IDQ. Якщо що-небудь, це добре на Skylake, оскільки цикл 9 або 10 взагалі не виходить оптимально за 4 уп кожного циклу . Це, мабуть, частина того, чому заміни lodsd
на pop
допомогли так сильно. (LSD не може зафіксувати uops, оскільки це не залишить місця для вставки синхронізації стека взагалі .) (BTW, оновлення мікрокоду відключає LSD повністю на Skylake та Skylake-X, щоб виправити помилку. Я виміряв значення вище, перш ніж отримати це оновлення.)
Я профілював його на Haswell і виявив, що він працює в 381,31 мільярда тактових циклів (незалежно від частоти процесора, оскільки він використовує лише кеш L1D, а не пам'ять). Пропускна здатність випуску на передньому кінці становила 3,72 Uops з плавним доменом на добу, порівняно з 3,70 для Skylake. (Але, звичайно, інструкції за цикл знизилися до 2,42 з 2,87, тому що adc
і cmov
на Haswell 2 уп.)
push
заміна, stosd
ймовірно, не допоможе так сильно, тому adc [esp + edx]
що викликає синхронізацію стека взагалі кожного разу. І коштував би байт, std
тому lodsd
йде в іншому напрямку. ( mov [edi], eax
/ lea edi, [edi+4]
для заміни stosd
- це виграш - від 32909 мотоциклів на 100 М ітерах до 31954 мотоциклів на 100 М ітерах. Схоже, що stosd
декодується як 3 уп, при цьому сховища адреси магазину / сховища даних не є мікроплавленими, тому push
+ стек-синхронізація Uops, можливо, буде швидше, ніж stosd
)
Фактична продуктивність ~ 322,47 мільярда циклів для 1G ітерацій 114 кінцівок працює до 2 824 циклів за ітерацію внутрішньої петлі , для швидкої версії 105B на Skylake. (Див. ocperf.py
Вихід нижче). Це повільніше, ніж я прогнозував у статичному аналізі, але я ігнорував накладні витрати зовнішньої петлі та будь-які Uop-синхронізації стека.
Perf лічильники branches
і branch-misses
показують, що внутрішній цикл неправильно прогнозується один раз на зовнішній цикл (на останній ітерації, коли він не зроблений). На це також припадає частина додаткового часу.
Я міг би зберегти розмір коду, зробивши, щоб внутрішній цикл мав тривалість затримки для критичного шляху, використовуючи mov esi,eax
/ sub eax,ebp
/ cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) замість lea esi, [eax - 1000000000]
/ cmp ebp,eax
/ cmovc
(6 + 2 + 3 = 11B ). cmov
/ stosd
Виключений критичний шлях. (Приріст-edi взагалі stosd
може працювати окремо від магазину, тому кожна ітерація відщеплює короткий ланцюг залежностей.) Він використовував для збереження іншого 1B, змінюючи інструкцію ebp init з lea ebp, [ecx-1]
на mov ebp,eax
, але я виявив, що помилкаebp
не змінив результат. Це дозволить кінцівці точно == 1000000000 замість того, щоб загортати та виробляти перенесення, але ця помилка поширюється повільніше, ніж у нас зростає Fib (), тому це не відбувається, щоб змінити провідні 1k цифри кінцевого результату. Крім того, я вважаю, що помилка може виправити себе, коли ми просто додаємо, оскільки є місце в кінцівці, щоб утримати її без переповнення. Навіть 1G + 1G не переповнює 32-бітове ціле число, тож воно в підсумку проникне вгору або буде усічене.
Версія 3с затримки - це 1 додатковий загальний вигляд, тому передній кінець може видавати її за один раз на 2,75 циклу на Skylake, лише трохи швидше, ніж задній. (У Haswell це буде 13 уп, оскільки він досі використовує adc
та cmov
, і вузьке місце на передньому кінці - 3,25c на ітер).
На практиці він працює на 1,18 повільніше на Skylake (3,34 цикла на кінцівку), а не 3 / 2,5 = 1,2, який я передбачив, щоб замінити переднє місце вузьким місцем затримкою, ніж просто дивитися на внутрішню петлю без синхронізації упс. Оскільки Uops стеки-синхронізації завдають шкоди тільки швидкій версії (вузькі місця на передній частині замість затримки), для її пояснення не потрібно багато. наприклад 3 / 2,54 = 1,18.
Іншим фактором є те, що версія затримки 3c може виявити помилковий прогноз щодо виходу з внутрішнього циклу, поки критичний шлях все ще виконується (оскільки передній кінець може випереджати його від заднього, дозволяючи виконанню поза замовленням запуску циклу- зустрічний uops), тому ефективний неправильний прогноз є меншим. Втрата цих циклів на передньому кінці дозволяє наздогнати його.
Якби не це, ми, можливо, могли б прискорити 3c- cmc
версію, використовуючи гілку у зовнішньому циклі замість безвідвідного поводження з компенсацією transfer_out -> edx та esp. Прогнозування гілки + спекулятивне виконання для залежності управління замість залежності даних могло б дати можливість наступній ітерації почати adc
цикл, поки Uops з попереднього внутрішнього циклу все ще знаходився в польоті. У безгалузевій версії адреси навантаження у внутрішньому циклі мають залежність даних від CF від останньої adc
з останньої кінцівки.
2с затримка внутрішньої петлі у вузькій версії на передній частині, тому задній кінець майже не відстає. Якщо код зовнішньої петлі був з високою затримкою, передній кінець може випереджати видачу Uops від наступної ітерації внутрішнього циклу. (Але в цьому випадку у зовнішньому циклі є багато ILP та відсутність високозатримки, тож у зворотному кінці не так вже й багато чого робити, коли він починає пережовувати Uops у планувальнику поза замовлення, як їх вхід стає готовим).
### 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 %)
- це стандартне відхилення протягом 4 прогонів для цього рахунку. Цікаво, що він виконує таку кругленьку кількість інструкцій. Ці 924 мільярди - не випадковість. Я здогадуюсь, що зовнішній цикл виконує загалом 924 інструкції.
uops_issued
є числом злитих доменів (актуально для пропускної спроможності випуску), в той час uops_executed
як кількість незрощених доменів (кількість Uops, що надсилаються на порти виконання). Micro-Fusion пакує 2 Uops з конденсованим доменом в один злитий домен взагалі, але mov-усунення означає, що деяким Uops з плавленим доменом не потрібні порти виконання. Дивіться пов’язане запитання, щоб отримати докладнішу інформацію про підрахунок Uops та злитого до неплавленого домену. (Також дивіться таблиці інструкцій Agner Fog та посібник uarch та інші корисні посилання у вікі тегів SO x86 ).
З іншого запуску вимірювання різних речей: пропуски кеш-пам’яті L1D абсолютно незначні, як очікується, для читання / запису тих самих двох буферів 456B. Гілка внутрішнього циклу неправильно прогнозує один раз на зовнішню петлю (коли не потрібно брати цикл). (Загальний час вище, тому що комп'ютер не був повністю без роботи. Напевно, інший логічний ядро деякий час був активним, і більше часу проводилося в перервах (оскільки частота вимірюваного простору користувача була нижче 4,400 ГГц). Або декілька ядер були активнішими більше часу, знижуючи макс. Турбо. Я не відслідковував, cpu_clk_unhalted.one_thread_active
чи є проблема конкуренції HT.)
### 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
Мій код цілком може працювати на меншій кількості циклів на Ryzen, що може видавати 5 уоп за цикл (або 6, коли деякі з них мають 2-загальні інструкції, як AVX 256b на Ryzen). Я не впевнений, що зробить його фронт-енд stosd
, це 3 уп на Ryzen (те саме, що і Intel). Я думаю, що інші вказівки у внутрішній петлі - це така ж затримка, що і Skylake та всі однократні. (У тому числі adc eax, [edi+edx]
, що є перевагою перед Skylake).
Це, ймовірно, може бути значно меншим, але, можливо, на 9 разів повільніше, якби я зберігав числа як 1 десяткова цифра на байт . Генерування з допомогою cmp
та коригування з цим cmov
буде працювати однаково, але зробіть 1/9 роботу. 2 десяткові цифри на байт (база-100, а не 4-бітний BCD з повільнимDAA
) також працюватимуть, і div r8
/ add ax, 0x3030
перетворює 0-99 байт на дві цифри ASCII в порядку друку. Але 1 цифра на байт взагалі не потрібна div
, просто циклічне додавання та додавання 0x30. Якщо я зберігаю байти в порядку друку, це зробить другий цикл справді простим.
Використання 18 або 19 десяткових цифр на 64-бітне ціле число (в 64-бітному режимі) змусить його працювати приблизно вдвічі швидше, але коштує значного розміру коду для всіх префіксів REX і для 64-бітних констант. 32-бітні кінцівки в 64-бітному режимі не дозволяють використовувати pop eax
замість цього lodsd
. Я все-таки міг уникати префіксів REX, використовуючи esp
як регістр подряпин, що не вказують (заміняючи використання esi
та esp
), замість того, щоб використовувати r8d
як 8-й регістр.
Якщо робити версію з функцією дзвінка, перетворення на 64-бітну та використання r8d
може бути дешевше, ніж збереження / відновлення rsp
. 64-бітний також не може використовувати однобайтове dec r32
кодування (оскільки це префікс REX). Але в основному я закінчив використовувати dec bl
2 байти. (Оскільки у мене є константа у верхніх байтах ebx
, і я використовую її лише за межами внутрішніх циклів, що працює, оскільки низький байт постійної 0x00
.)
Високопродуктивна версія
Для досягнення максимальної продуктивності (не для коду-гольфу), ви хочете розкрутити внутрішній цикл, щоб він працював щонайменше за 22 ітерацій, що є досить коротким, прийнятим / неприйнятим шаблоном для того, щоб гілки-прогнозисти спрацювали добре. У моїх експериментах mov cl, 22
перед .inner: dec cl/jnz .inner
циклом є дуже мало помилок (наприклад, 0,05%, що набагато менше одного за повний пробіг внутрішньої петлі), але mov cl,23
неправильно прогнозує від 0,35 до 0,6 разів за внутрішній цикл. 46
особливо погано, непередбачувано ~ 1,28 рази за внутрішню петлю (128М разів для 100М ітерацій зовнішньої петлі). 114
неправильно передбачили рівно один раз на внутрішню петлю, те саме, що я виявив у складі циклу Фібоначчі.
Мені стало цікаво і спробував це, розгорнувши внутрішню петлю на 6 з а %rep 6
(бо це розділяє 114 рівномірно). Це здебільшого усувало недоліки в галузі. Я зробив edx
негатив і використовував це як компенсацію для mov
магазинів, щоб adc eax,[edi]
міг залишатися мікроплавким. (І так я міг уникнути stosd
). Я витягнув lea
оновлення edi
з %rep
блоку, тож він робить лише одне оновлення покажчика на 6 магазинів.
Я також позбувся всіх матеріалів з частковим реєстром у зовнішній петлі, хоча не думаю, що це було суттєвим. Можливо, це трохи допомогло, щоб CF на кінці зовнішньої петлі не залежав від кінцевої АЦП, тому деякі внутрішні петлі можуть бути розпочаті. Код зовнішньої петлі, можливо, можна було б оптимізувати трохи більше, оскільки це neg edx
було останнє, що я зробив, замінивши xchg
лише дві mov
інструкції (оскільки у мене вже було 1), і перевпорядкувавши ланцюги dep разом із скиданням 8-розрядних реєструвати речі.
Це джерело NASM саме з циклу Фібоначчі. Це заміна, що випадає для цього розділу оригінальної версії.
;;;; 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
Продуктивність:
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% )
Це для тієї ж Fib (1G), що дає той же вихід за 62,3 секунди замість 73 секунд. (Цикли 273.146G проти 322.467G. Оскільки все потрапляє в кеш L1, базові цикли годин - це все, що нам потрібно подивитися).
Зверніть увагу на значно менший загальний uops_issued
підрахунок, що значно нижче uops_executed
. Це означає, що багато з них були мікроплавкими: 1 взагалі у злитому домені (випуск / ROB), але 2 уоп у неплавленому домені (планувальник / одиниці виконання). І мало хто був усунутий на етапі випуску / перейменування (наприклад, mov
копіювання реєстру або xor
-zeroing, яке потрібно видавати, але не потребує блоку виконання). Усунуті uops не врівноважують кількість рахунків в інший спосіб.
branch-misses
знижується до ~ 400k, з 1G, тому розгортання працювало. resource_stalls.any
зараз важливо, а це означає, що передня частина вже не є вузьким місцем: натомість задній кінець відстає і обмежує передню частину. idq_uops_not_delivered.core
підраховує лише цикли, коли фронтальний наконечник не видав Uops, але задній не був застопорений. Це приємно і низько, що вказує на кілька передніх вузьких місць.
Веселий факт: версія python витрачає більше половини свого часу, діючи на 10, а не додаючи. (Заміна a/=10
зі a>>=64
швидкістю його більш ніж в 2 рази, але змінює результат , тому що двійковий урізання = десяткове усічення.)
Моя версія ASM, звичайно, оптимізована спеціально для цього розміру проблеми, ітерація циклу - важко кодована. Навіть зміщення довільно точного числа скопіює його, але моя версія може просто прочитати зі зміщення для наступних двох ітерацій, щоб пропустити навіть це.
Я профілював версію python (64-бітний python2.7 в 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
Числа в (паронах) - це кількість часу, на який проводили вибірку лічильника парфумів. Якщо дивитися на більше лічильників, ніж опори HW, перф обертається між різними лічильниками та екстраполятами. Це абсолютно добре для тривалого виконання одного і того ж завдання.
Якби я побіг perf
після встановлення sysctl kernel.perf_event_paranoid = 0
(або запустив perf
як root), він міряв би 4.400GHz
. cycles:u
не враховує час, витрачений на переривання (або системні виклики), лише цикли простору користувача. Мій робочий стіл майже повністю працював, але це типово.
Your program must be fast enough for you to run it and verify its correctness.
як щодо пам’яті?