x86 32ビットマシンコード(Linuxシステムコールを使用):106 105バイト
changelog:オフバージョンの定数はFib(1G)の結果を変更しないため、高速バージョンで1バイトを保存しました。
または102は、(使用して18%より遅い(Skylakeマイクロアーキテクチャ上の)バージョンのバイトmov
/ sub
/ cmc
の代わりにlea
/ cmp
で搬出及び包装を生成するために、内側ループに10**9
代わりに2**32
)。または、最内ループのキャリー処理に分岐を含む〜5.3x低速バージョンの場合は101バイト。(25.4%の分岐予測率を測定しました!)
または、先行ゼロが許可されている場合は104/101バイト。(出力の1桁をスキップするハードコードに1バイト余分に必要です。これは、Fib(10 ** 9)に必要なことです)。
残念ながら、TIOのNASMモード-felf32
はコンパイラフラグで無視されるようです。 とにかく、コメントに実験的なアイデアの混乱をすべて含む、完全なソースコードとのリンクがあります。
これは完全なプログラムです。Fib(10 ** 9)の最初の1000桁に続いて、余分な数字(最後の数桁が間違っている)に続いて、ごみバイト(改行を含まない)が出力されます。ガベージのほとんどは非ASCIIなので、を介してパイプすることもできますcat -v
。konsole
ただし、端末エミュレーター(KDE )は壊れません。「ガベージバイト」はFib(999999999)を格納しています。私はすでに-1024
レジスタを持っているので、適切なサイズよりも1024バイトを印刷する方が安価でした。
マシンコード(静的実行可能ファイルのテキストセグメントのサイズ)だけをカウントしています。ELF実行可能ファイルにするための綿毛はカウントしていません。(非常に小さなELF実行可能ファイルが可能ですが、それを気にしたくありませんでした)。BSSの代わりにスタックメモリを使用する方が短いことがわかったので、メタデータに依存しないため、バイナリ内の他のものを数えないことを正当化できます。(通常の方法でストリップされた静的バイナリを生成すると、340バイトのELFが実行可能になります。)
Cから呼び出すことができるこのコードから関数を作成できます。スタックポインター(MMXレジスタ内など)とその他のオーバーヘッドを保存/復元するには数バイトかかりますが、文字列を返すことでバイトを節約することもできます。write(1,buf,len)
システムコールを行う代わりに、メモリ内で。マシンコードでのゴルフは、ネイティブ拡張精度を使用せずに他の言語で回答を投稿した人もいないため、ここで多少の余裕があると思いますが、この機能バージョンは全体を再ゴルフすることなく、120バイト未満である必要があります事。
アルゴリズム:
ブルートフォースa+=b; swap(a,b)
、必要に応じて切り捨てて、先頭の1017桁以上の10進数のみを保持します。私のコンピューターでは1分13秒(または322.47億クロックサイクル±0.05%)で実行されます(さらに、コードサイズの追加バイト数で数%速くなるか、ループ展開からのコードサイズがはるかに大きい62 秒まで短縮されます。巧妙な数学、より少ないオーバーヘッドで同じ仕事をするだけです)。それは私のコンピューター(4.4GHz Skylake i7-6700k)で12min35sで実行される@AndersKaseorgのPython実装に基づいています。どちらのバージョンにもL1Dキャッシュミスがないため、DDR4-2666は関係ありません。
Pythonとは異なり、拡張精度の数値は、小数点以下を切り捨てられる形式で保存します。32ビット整数ごとに9桁の10進数のグループを格納するため、ポインターオフセットは下位9桁を破棄します。これは事実上10億の基数であり、10の累乗です(このチャレンジには10億のフィボナッチ数が必要であることはまったく偶然ですが、2つの独立した定数に対して数バイト節約できます)。
GMPの用語に従って、拡張精度の数値の各32ビットチャンクは「リム」と呼ばれます。追加中のキャリーアウトは、1e9と比較して手動で生成する必要がありADC
ますが、次の肢の通常の指示への入力として通常使用されます。([0..999999999]
2 ^ 32〜= 4.295e9ではなく、手動で範囲にラップする必要があります。比較のキャリーアウト結果を使用して、lea
+ cmov
でブランチレスにこれを行います。)
最後のリムがゼロ以外のキャリーアウトを生成する場合、外側のループの次の2回の繰り返しは、通常よりも1リム高い値から読み取りますが、同じ場所に書き込みます。これはmemcpy(a, a+4, 114*4)
、1つ右にシフトするのと似ていますが、次の2つの加算ループの一部として行われます。これは、18回の反復ごとに発生します。
サイズ節約とパフォーマンスのためのハック:
私がそれを知っているときのlea ebx, [eax-4 + 1]
代わりにmov ebx, 1
、のような通常のものeax=4
。またloop
、LOOP
の遅さがわずかな影響しか与えない場所で使用します。
adc
内側のループのバッファーの先頭に書き込みながら、読み取り元のポインターをオフセットすることにより、自由に1リムずつ切り捨てます。から読み取り[edi+edx]
、書き込み[edi]
ます。したがって、宛先の読み取り/書き込みオフセットを取得edx=0
または4
取得できます。これを2回連続して繰り返し、最初に両方をオフセットし、次にdstのみをオフセットする必要があります。2番目のケース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以降では、low8レジスタの部分レジスタマージペナルティはありません(実際、個別に名前を変更することさえしません)。したがって、AMDのように、フルレジスタに依存しています(ここでは問題ありません)。ただし、Nehalem以前では、マージ時に部分的なレジスタストールがあるため、これは恐ろしいことです。部分的なregをxor
作成し、-zeroingまたはa なしで完全なregを読み取る他の場所がありますmovzx
、通常、以前のコードが上位バイトをゼロにしたことを知っているためです。AMDおよびIntel SnBファミリでは問題ありませんが、Intel pre-Sandybridgeでは低速です。
私は1024
stdout(sub edx, ebx
)に書き込むためのバイト数として使用しmov edx, 1000
ます。そのため、より多くのバイトが必要になるため、プログラムはフィボナッチ桁の後にゴミバイトを出力します。
(使用されません)adc ebx,ebx
EBX = 0でEBX = CFを取得し、1バイトを節約しますsetc bl
。
dec
/ ループjnz
内では、Intel Sandybridge以降でフラグを読み取るadc
ときに部分フラグストールを引き起こすことなくCFを保持しadc
ます。 初期のCPUでは悪いですが、Skylakeでは無料です。または最悪の場合、余分なuop。
以下のメモリをesp
巨大なレッドゾーンとして使用します。これは完全なLinuxプログラムであるため、シグナルハンドラーをインストールしなかったこと、およびユーザー空間のスタックメモリを非同期に上書きするものはないことを知っています。これは、他のOSでは当てはまらない場合があります。
活用して、スタック・エンジン使用してUOP問題の帯域幅を節約するためにpop eax
(1つのUOP +時折スタック同期UOP)の代わりにlodsd
(によるIVBのハスウェル/ Skylakeマイクロアーキテクチャ、3の2つのuopおよびそれ以前のAgner霧の命令テーブル))。IIRCでは、実行時間が約83秒から73に短縮されました。src とdstバッファ間のオフセットを保持する場所のmov
ように、インデックス付きアドレッシングモードでa を使用してもおそらく同じ速度が得られます。(内部ループの外側のコードはより複雑になり、フィボナッチ反復のsrcとdstの交換の一部としてオフセットレジスタを無効にする必要があります。)詳細については、以下の「パフォーマンス」セクションを参照してください。mov eax, [edi+ebp]
ebp
をメモリ内のどこかにstc
保存する代わりに、最初の反復にキャリーイン(1バイト)を与えることでシーケンスを開始します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
おそらくこれからさらにバイトをゴルフする余地がありますが、私はすでに2日間でこれに少なくとも12時間を費やしました。 速度が十分に速いとはいえ、速度を犠牲にしたくありませんし、速度を犠牲にするほど小さくする余地があります。投稿の理由の一部は、ブルートフォースのasmバージョンを作成できる速さを示すことです。本当に最小サイズにしたいが、おそらく10倍遅い(たとえば、1バイトあたり1桁)ことを望む人は、これを出発点として自由にコピーしてください。
結果の実行可能ファイル(からyasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
)は340B(削除)です。
size fibonacci-1G
text data bss dec hex filename
105 0 0 105 69 fibonacci-1G
性能
内部adc
ループは、Skylakeで10個の融合ドメインuop(128バイトごとに+1スタック同期uop)であるため、最適なフロントエンドスループットでSkylakeで〜2.5サイクルごとに1回発行できます(スタック同期uopを無視) 。adc
-> cmp
->次の反復のadc
ループキャリー依存チェーンのクリティカルパス遅延は2サイクルであるため、ボトルネックは反復ごとに〜2.5サイクルのフロントエンドの問題の制限である必要があります。
adc eax, [edi + edx]
実行ポート用の2つの非融合ドメインuop:load + ALU。デコーダーではマイクロヒューズ(1 つの融合ドメインuop)が発生しますが、Haswell / Skylakeでもインデックス付きアドレッシングモードのため、発行段階では2つの融合ドメインuopにラミネートされません。私はそれがそうであるように、マイクロ融合したままになると思ったがadd eax, [edi + edx]
、多分、インデックス付きアドレス指定モードを維持することは、すでに3つの入力(フラグ、メモリ、および宛先)を持っているuopに対しては機能しない。書いたとき、パフォーマンスにマイナス面はないと思っていましたが、間違っていました。この切り捨ての処理方法edx
は、0または4 であるかどうかに関係なく、毎回内側のループを遅くします。
オフセットedi
を使用edx
してストアを調整することにより、dstの読み取り/書き込みオフセットを処理する方が高速です。だからadc eax, [edi]
/ ... / mov [edi+edx], eax
/のlea edi, [edi+4]
代わりにstosd
。Haswell以降では、インデックス化されたストアをマイクロ融合状態に保つことができます。(Sandybridge / IvBもそれをラミネート解除します。)
Intel Haswell以前ではadc
、cmovc
それぞれ2 uopsであり、レイテンシは2cです。(adc eax, [edi+edx]
Haswellではまだラミネートされておらず、3つの融合ドメインuopとして問題があります)。Broadwellマイクロアーキテクチャ以降は3入力だけFMA(ハスウェル)以上のためのuop、作ることを可能adc
とcmovc
彼らは長い間、AMDにされているように、(カップル他のもの)のシングルUOPの指示を。(これが、AMDが拡張精度GMPベンチマークで長い間うまく行っている理由の1つです。)とにかく、Haswellの内部ループは12 uop(時々スタック同期uop +1)で、フロントエンドのボトルネックは1スタック同期uopを無視して、最適なケースを繰り返します。
ループ内でpop
バランシングなしで使用push
すると、ループはLSD(ループストリームディテクター)から実行できなくなり、uopキャッシュからIDQに毎回再読み取りする必要があります。9または10 uopループはサイクルごとに4 uopsで最適に発行されないため、 Skylakeではそれが良いことです。これはおそらく、交換理由の一部であるlodsd
とpop
そんなに助けました。(スタック同期 uopを挿入する余地を残さないため、LSDはuopをロックダウンできません。)(BTW、マイクロコードの更新により、SkylakeとSkylake-XでLSDが完全に無効になり、エラッタが修正されます。上記のアップデートを取得する前に。)
Haswellでプロファイルを作成したところ、(メモリではなくL1Dキャッシュのみを使用しているため、CPU周波数に関係なく)3813億1,000万クロックサイクルで実行されることがわかりました。フロントエンドの問題のスループットは、Skylakeの3.70に対して、クロックあたり3.72融合ドメインuopsでした。(もちろん、Haswellでは2 uopであるためadc
、サイクルあたりの命令は2.87から2.42に減少しましたcmov
。)
push
スタックシンクuopを毎回トリガーするstosd
ため、交換することはおそらくあまり役に立ちませんadc [esp + edx]
。また、1バイトかかるstd
ためlodsd
、逆方向に進みます。(mov [edi], eax
/ lea edi, [edi+4]
を置き換えるstosd
と勝利となり、1億反復の32,909Mサイクルから1億反復の31,954Mサイクルになります。3 stosd
つのuopとしてデコードされ、store-address / store-data uopsはマイクロ融合されないため、push
+ stack-sync uopsはまだより高速かもしれませんstosd
)
Skylakeの高速105Bバージョンでは、114肢の1G反復で〜322.47億サイクルの実際のパフォーマンスは、内側ループの反復あたり2.824サイクルになります。(ocperf.py
以下の出力を参照してください)。それは静的解析から予測したよりも遅いですが、外側のループとスタック同期uopのオーバーヘッドを無視していました。
Perf は、内側のループが外側のループごとに1回(最後の反復で、それが実行されない場合)誤予測することbranches
をbranch-misses
示します。それは余分な時間の一部も占めています。
Iは、最も内側のループを使用して、クリティカル・パスのために3サイクルの待ち時間を有することにより、コードサイズを節約できる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
クリティカルパスから外れています。(の増分編集uopはstosd
ストアとは別に実行できるため、各反復は短い依存関係チェーンから分岐します。)以前はebp init命令をからlea ebp, [ecx-1]
に変更して別の1Bを保存mov ebp,eax
していましたが、間違っていることがわかりましたebp
結果は変わりませんでした。これにより、四肢はラップしてキャリーを生成する代わりに== 1000000000になりますが、このエラーはFib()が成長するよりも遅く伝播するため、最終結果の先頭の1k桁は変更されません。また、手足にはオーバーフローせずに保持する余地があるため、追加するだけでエラーが修正されると思います。1G + 1Gでさえ32ビット整数をオーバーフローさせないため、最終的には上に浸透するか、切り捨てられます。
3cレイテンシバージョンは1 uop追加であるため、フロントエンドはSkylakeで2.75cサイクルごとに1回発行できますが、バックエンドが実行できるよりもわずかに高速です。(Haswellでは、まだとを使用adc
しているため、合計で13 uopsになりますcmov
。フロントエンドのボトルネックは、iterあたり3.25cです)。
実際には、Skylakeでは1.18倍遅くなります(手足あたり3.34サイクル)。スタック同期なしで内部ループを見るだけでフロントエンドのボトルネックをレイテンシボトルネックに置き換えると予測した3 / 2.5 = 1.2ではなく、おっと。スタック同期uopは高速バージョン(レイテンシの代わりにフロントエンドでボトルネック)を損なうだけなので、それを説明するのにそれほど時間はかかりません。例:3 / 2.54 = 1.18。
もう1つの要因は、3cレイテンシバージョンが、クリティカルパスの実行中に内部ループを離れる際の予測ミスを検出する可能性があることです(フロントエンドがバックエンドよりも先に進み、アウトオブオーダー実行がループを実行できるためです-カウンターuops)、したがって、効果的な予測ミスのペナルティは低くなります。これらのフロントエンドサイクルを失うと、バックエンドが追いつきます。
そうでない場合cmc
は、carry_out-> edxおよびespオフセットをブランチレスで処理する代わりに、外側のループでブランチを使用することで、3c バージョンを高速化できます。分岐予測+データ依存関係ではなく制御依存関係の投機的実行adc
により、前の内部ループからのuopsがまだ実行されている間に次の反復がループの実行を開始する可能性があります。ブランチレスバージョンでは、内部ループのロードアドレスadc
は、最後の肢の最後からCFへのデータ依存関係を持ちます。
2cレイテンシのインナーループバージョンはフロントエンドでボトルネックになるため、バックエンドはほとんど追いつきません。外側のループコードのレイテンシが高い場合、フロントエンドは、内側のループの次の反復からuopを発行して先に進む可能性があります。(ただし、この場合、アウターループのものには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回の実行に対する標準偏差です。興味深いことに、このようなラウンド数の命令を実行します。その9,240億は偶然ではありません。外側のループは合計924命令を実行していると思います。
uops_issued
融合ドメインカウント(フロントエンドの問題の帯域幅に関連)でuops_executed
あり、非融合ドメインカウント(実行ポートに送信されたuopの数)です。マイクロフュージョンは、2つのアンフューズドドメインuopを1つのフューズドドメインuopにパックしますが、mov-eliminationは、一部のフューズドドメインuopsが実行ポートを必要としないことを意味します。uopと融合ドメインと非融合ドメインのカウントについて詳しくは、リンクされた質問をご覧ください。(Agner Fogの手順表とuarchガイド、およびSO x86タグwikiの他の便利なリンクも参照してください)。
さまざまなことを測定する別の実行から:同じ2つの456Bバッファーの読み取り/書き込みで予想されるように、L1Dキャッシュミスはまったく重要ではありません。内側のループ分岐は、外側のループごとに1回予測を誤ります(ループから抜けない場合)。(コンピューターが完全にアイドル状態ではなかったため、合計時間が長くなりました。おそらく、他の論理コアがいくつかの時間アクティブであり、割り込みに多くの時間が費やされました(ユーザー空間で測定された周波数が4.400GHzをはるかに下回っていたため)。または、複数のコアがより頻繁にアクティブになり、最大ターボが低下しました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 uop(または、RyzenのAVX 256bなどの2 uop命令の場合は6 uop)を発行できます。フロントエンドがstosd
Ryzenで3 uop(Intelと同じ)で何をするのかわかりません。内部ループの他の命令は、Skylakeおよびすべての単一uopと同じレイテンシーだと思います。(adc eax, [edi+edx]
Skylakeよりも有利なを含む)。
これはおそらく大幅に小さくなる可能性がありますが、バイトごとに1桁の10進数として数値を保存すると、9倍遅くなる可能性があります。キャリーアウトの生成cmp
と調整cmov
は同じように機能しますが、作業の1/9を行います。1バイトあたり2桁の10進数(base-100、低速のDAA
4ビットBCDではない)も機能し、div r8
/ add ax, 0x3030
は0-99バイトを印刷順序で2つのASCII数字に変換します。ただし、バイトごとに1桁は必要ありませんdiv
。ループして0x30を追加するだけです。バイトを印刷順に保存すると、2番目のループが非常に簡単になります。
64ビット整数ごとに18桁または19桁の10進数を使用すると(64ビットモードで)約2倍の速度で実行されますが、すべてのREXプレフィックスと64ビット定数のコードサイズが大きくなります。64ビットモードの32ビットリムでは、のpop eax
代わりに使用できませんlodsd
。8番目のレジスタとして使用する代わりにesp
、非ポインタースクラッチレジスタとして使用することで、REXプレフィックスを回避できました(esi
and の使用法を交換しますesp
)r8d
。
呼び出し可能関数バージョンを作成する場合、64ビットに変換して使用r8d
する方が、保存/復元するよりも安くなる場合がありますrsp
。64ビットでは、1バイトdec r32
エンコーディングを使用できません(REXプレフィックスであるため)。しかし、ほとんどの場合、dec bl
2バイトを使用することになりました。(の上位バイトに定数があり、ebx
それを内部ループの外側でのみ使用しているため、定数の下位バイトがであるため機能します0x00
。)
高性能バージョン
最大のパフォーマンス(コードゴルフではない)を得るには、最大22回の反復を実行するように内側のループを展開する必要があります。これは、分岐予測子がうまく機能するために十分に短いパターンです。私の実験ではmov cl, 22
、.inner: dec cl/jnz .inner
ループの予測ミスはほとんどありませんが(0.05%など、内部ループの完全な実行ごとに1未満)、mov cl,23
内部ループごとに0.35から0.6回の予測ミスがあります。 46
特に悪いのは、内部ループごとに〜1.28回(100Mの外部ループの繰り返しで128M回)の予測ミスです。 114
フィボナッチループの一部として見つけたのと同じように、内側のループごとに1回だけ予測ミスをしました。
私は好奇心got盛になり、それを試してみました%rep 6
(内側のループを114で均等に分割するため)。これにより、分岐ミスがほとんどなくなりました。edx
ネガを作成し、mov
店舗のオフセットとして使用したので、adc eax,[edi]
マイクロフューズのままにすることができました。(そして、私は避けることができましたstosd
)。をlea
更新edi
して%rep
ブロックから更新したため、6つのストアごとに1つのポインター更新のみを実行します。
また、外側のループ内のすべての部分レジスタを削除しましたが、それは重要ではないと思います。最終的なADCに依存しない外側のループの終わりにCFを置くと少し助けになったので、内側のループuopの一部を開始できます。アウターループコードはおそらくもう少し最適化できます。これneg edx
は、xchg
たった2つのmov
命令に置き換えた後(私はまだ1つを持っているため)、8ビットをドロップするとともにdepチェーンを再配置した後、最後にやったことだったからですものを登録します。
これは、フィボナッチループの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)の場合で、73秒ではなく62.3秒で同じ出力を生成します。(273.146Gサイクル、対322.467G。すべてがL1キャッシュでヒットするので、コアクロックサイクルだけを見る必要があります。)
総数をはるかに下回る合計uops_issued
数に注意してくださいuops_executed
。これは、それらの多くがマイクロ融合されたことを意味します。融合ドメイン(issue / ROB)では1 uopですが、非融合ドメイン(scheduler / execution units)では2 uopです。そして、そのいくつかは、発行/名前変更の段階で削除されました(発行mov
コピーxor
は必要ですが、実行ユニットは不要です)。uopを削除すると、逆にカウントのバランスが崩れます。
branch-misses
1Gから約40万まで低下しているため、アンロールが機能しました。 resource_stalls.any
これは、フロントエンドがボトルネックではなくなったことを意味します。代わりに、バックエンドが遅れてフロントエンドを制限しています。 idq_uops_not_delivered.core
フロントエンドがuopを配信しなかったが、バックエンドがストールしなかったサイクルのみをカウントします。これは素晴らしく低いもので、フロントエンドのボトルネックがほとんどないことを示しています。
おもしろい事実:Pythonバージョンは、追加するのではなく10で割った時間の半分以上を費やしています。(a/=10
withを置き換えるa>>=64
と2倍以上高速になりますが、バイナリ切り捨て!= 10進切り捨てのため結果が変わります。)
もちろん、私のasmバージョンは、この問題サイズに合わせて最適化されており、ループの反復カウントはハードコーディングされています。任意精度の数値をシフトしてもコピーされますが、私のバージョンでは、次の2回の反復でそれをスキップするためにオフセットから読み取ることができます。
Pythonバージョン(Arch Linuxの64ビットpython2.7)のプロファイルを作成しました。
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は異なるカウンターと外挿の間を回転します。同じタスクを長期間実行する場合は、これでまったく問題ありません。
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.
メモリはどうですか?