x86 32位机器码(使用Linux系统调用):106个 105字节
changelog:在快速版本中保存了一个字节,因为一个不加常数不会改变Fib(1G)的结果。
或102字节的速度(在Skylake上)慢18%(在内部循环中使用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
。但是,它不会破坏我的终端仿真器(KDE konsole
)。“垃圾字节”存储Fib(999999999)。我已经-1024
在寄存器中了,所以打印1024个字节比适当的大小便宜。
我只计算机器代码(静态可执行文件的文本段的大小),而不计算使它成为ELF可执行文件的绒毛。(很小的ELF可执行文件是可能的,但我不想为此烦恼)。事实证明,使用堆栈内存而不是BSS会更短,因此我可以证明不对二进制文件中的其他内容进行计数是合理的,因为我不依赖任何元数据。(以常规方式生成剥离的静态二进制文件使340字节的ELF可执行文件。)
您可以使用可以从C调用的代码来创建函数。保存/恢复堆栈指针(可能在MMX寄存器中)和其他一些开销将花费一些字节,但是通过返回字符串可以节省字节在内存中,而不是进行write(1,buf,len)
系统调用。我认为打高尔夫球的机器代码应该让我有些懈怠,因为没有其他人甚至没有使用本机扩展精度就可以发布任何语言的答案,但是我认为该函数的功能版本仍应小于120字节,而无需重新进行整体研究事情。
算法:
蛮力a+=b; swap(a,b)
,根据需要截断以仅保留前导> = 1017十进制数字。它在我的计算机上运行的时间为1min13s(或3224.7亿个时钟周期+-0.05%)(如果再增加一些代码大小的字节,运行速度可能会快几个百分点,而从循环展开到更大的代码大小,运行速度可能会降低62s。聪明的数学,只需花费更少的精力就可以完成相同的工作)。它基于@AndersKaseorg的Python实现,可在我的计算机(4.4GHz Skylake i7-6700k)上运行12分35秒。这两个版本都没有丢失任何L1D缓存,因此我的DDR4-2666没关系。
与Python不同,我将扩展精度数字以一种使小数位截断成为免费的格式存储。我每32位整数存储9位十进制数字的组,因此指针偏移量将丢弃低9位数字。这实际上是10亿的基数,即10的幂。(这完全是巧合,这个挑战需要10亿斐波那契数,但是与两个独立的常数相比,它确实为我节省了几个字节)。
按照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
通过偏移我们读取的指针,将第一个分支免费截断,同时仍写入adc
内部循环中缓冲区的开头。我们从阅读[edi+edx]
,然后写信给[edi]
。这样我们就可以获取edx=0
或4
获取目标的读写偏移量。我们需要进行两次连续的迭代,首先偏移两个,然后仅偏移dst。我们通过在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,然后读取完整的reg,而不用xor
-zeroing或movzx
,通常是因为我知道先前的一些代码将高位字节清零了,这在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及更高版本上读取标志时导致部分标志停顿。 这对于较早的CPU来说很糟糕,但是Skylake上免费提供AFAIK。或者最坏的情况是额外的麻烦。
将下面的内存esp
用作巨大的红色区域。由于这是一个完整的Linux程序,所以我知道我没有安装任何信号处理程序,并且没有别的会异步破坏用户空间堆栈内存。在其他操作系统上可能不是这种情况。
利用堆栈引擎的优势,通过使用pop eax
(1 uop +偶尔的堆栈同步uop)而不是lodsd
(根据Agner Fog的指令表,在Haswell / Skylake上为2 uops,在IvB上为3 uop )来节省uop发行带宽。IIRC,这将运行时间从约83秒减少到了73秒。使用mov
带有索引寻址模式的a可能会获得相同的速度,例如mov eax, [edi+ebp]
其中ebp
src和dst缓冲区之间的偏移量保持不变。(这会使内部循环外的代码变得更加复杂,因为在Fibonacci迭代中交换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个小时。 我不想牺牲速度,尽管它的速度远远不够快,并且还有空间以降低速度的方式减小速度。我发布帖子的部分原因是,展示了我可以以多快的速度制作暴力ASM版本。如果有人真的想最小化,但可能要慢10倍(例如,每字节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个融合域uops(每〜128字节+1个堆栈同步uop),因此它可以在Skylake上每2.5个周期发出一次,具有最佳的前端吞吐量(忽略栈同步uops) 。对于adc
-> cmp
->下一个迭代的adc
循环承载依赖链,关键路径延迟为2个周期,因此瓶颈应为每次迭代约2.5个周期的前端问题限制。
adc eax, [edi + edx]
对于执行端口,有2个未融合域的指令:load + ALU。它在解码器中微熔丝(1个融合域uop),但由于采用索引寻址模式,即使在Haswell / Skylake上,在发行阶段也会取消层压为2个融合域uop。我认为它会像以前一样保持微融合,add eax, [edi + edx]
但是也许保持索引寻址模式微融合对已经具有3个输入(标志,内存和目标)的uops无效。当我写它的时候,我以为它不会降低性能,但是我错了。这种处理截断的方法每次都会减慢内部循环的速度,无论edx
是0还是4。
通过偏移edi
并edx
用于调整存储,可以更快地处理dst的读写偏移。所以adc eax, [edi]
/ ... / mov [edi+edx], eax
/ lea edi, [edi+4]
代替stosd
。Haswell及其以后可以保持索引存储微融合。(Sandybridge / IvB也会将其分层。)
在英特尔的Haswell和更早版本,adc
并且cmovc
都为2个微指令,与2C延迟。(adc eax, [edi+edx]
在Haswell上仍未分层,发行为3个融合域uops)。Broadwell及其后的版本允许三输入微指令不仅用于FMA(Haswell),还可以进行单微指令adc
和cmovc
(以及其他一些事情)单微指令,就像它们在AMD上使用了很长时间一样。(这是AMD长期以来一直在扩展精度GMP基准测试中取得出色成绩的原因之一。)无论如何,Haswell的内部循环应为12 oups(有时为+1堆栈同步uop),前端瓶颈为每个3c最好的情况,忽略堆栈同步的指令。
使用pop
不带平衡push
环的内部意味着环路不能从LSD(循环流检测器)上运行,并已被从UOP缓存每次重新读入IDQ。如果有的话,这在Skylake上是一件好事,因为9或10 uop循环在每个周期4 uops时并不是最佳选择。这可能是为什么用替换lodsd
有pop
很大帮助的部分原因。(LSD无法锁定uops,因为那样就不会留出空间来插入堆栈同步uop。)(顺便说一句,微代码更新完全禁用了Skylake和Skylake-X上的LSD来修复勘误。我测量了在获得更新之前)。
我在Haswell上对其进行了分析,发现它可以运行3813.1亿个时钟周期(与CPU频率无关,因为它仅使用L1D高速缓存,而不使用内存)。前端问题的吞吐量为每个时钟3.72融合域微指令,而Skylake为3.70。(但是,当然,每个周期的指令从2.87下降到2.42,因为adc
和cmov
在Haswell上是2微秒。)
push
替换stosd
可能没有太大帮助,因为adc [esp + edx]
每次都会触发堆栈同步uop。并且将花费一个字节,std
因此lodsd
走了另一个方向。(mov [edi], eax
/ lea edi, [edi+4]
替换stosd
是一个胜利,从100M iters的32,909Mcycles到100M iters的31,954Mcycles。似乎stosd
解码为3 uops ,store-address / store-data uops没有微融合,因此push
+堆栈同步uops可能仍然比stosd
)快
在Skylake上的快速105B版本中,114条肢体的1G迭代的实际性能约为3224.7亿个周期,每次内循环迭代为2.824个周期。(请参见ocperf.py
下面的输出)。这比我从静态分析中预测的速度要慢,但是我忽略了外循环和任何堆栈同步控件的开销。
Perf进行计数branches
并branch-misses
显示出,每个外部循环都会对内部循环进行一次错误的预测(在上一次迭代中,如果不采用)。这也占了额外时间的一部分。
我可以通过使最内侧环保存代码大小具有关键路径,使用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
已关闭的关键路径。(增量-EDI的微指令stosd
可以从商店单独运行,所以每次迭代叉关闭短依赖链。)它使用从改变EBP INIT指令来保存另一个1B lea ebp, [ecx-1]
到mov ebp,eax
,但我发现,具有错ebp
没有改变结果。这将使肢体精确地== 1000000000,而不是包裹并产生一个进位,但是此错误的传播速度比我们Fib()的增长慢,因此这种情况不会改变最终结果的前1k位。另外,我认为在添加时该错误可以自行纠正,因为肢体中有足够的空间容纳它而不会溢出。即使1G + 1G也不会溢出32位整数,因此最终它会向上渗透或被截断。
3c延迟版本额外增加了1个uop,因此前端可以在Skylake上每2.75c周期发布一次,仅比后端运行它快一点。(在Haswell上,由于它仍使用adc
和cmov
,因此总共为13 ups,并且瓶颈在每iter 3.25c处在前端)。
在实践中,它在Skylake上的运行速度要慢1.18倍(每肢3.34个周期),而不是我预测的3 / 2.5 = 1.2,这是通过仅通过查看内部循环而不使用堆栈同步来将延迟瓶颈替换为延迟瓶颈哎呀。由于堆栈同步控件仅会损害快速版本(前端出现瓶颈而不是延迟),因此无需过多解释。例如3 / 2.54 = 1.18。
另一个因素是3c延迟版本可能会在关键路径仍在执行时检测到离开内部循环的错误预测(因为前端可以领先于后端,从而让乱序执行可以运行循环-计数器误操作),因此有效的误判惩罚较低。失去那些前端周期会使后端追上来。
如果不是那样的话,我们可以cmc
通过在外循环中使用分支来加快3c 版本的速度,而不是对进位-> edx和esp偏移量进行无分支处理。控制依赖而不是数据依赖的分支预测+投机执行可以使下一个迭代开始运行该adc
循环,而上一个内部循环的运行仍在进行中。在无adc
分支版本中,内部循环中的加载地址从最后一个分支的最后一个开始对CF具有数据依赖性。
2c延迟内环版本在前端出现瓶颈,因此后端几乎可以保持正常运行。如果外循环代码是高延迟的,则前端可以从内循环的下一次迭代中提前发出微指令。(但是在这种情况下,外循环的东西有很多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
而是一个非融合域计数(发送到执行端口的微指令数)。微融合将2个未融合域的uops打包到一个融合域的uop中,但是移动消除意味着某些融合域的uops不需要任何执行端口。请参阅链接的问题,以获取有关计数uops和融合与未融合域的更多信息。(另请参阅Agner Fog的说明表和uarch指南,以及SO x86标签Wiki中的其他有用链接)。
从另一次测量不同情况的运行来看:L1D高速缓存未命中完全无关紧要,这与读取/写入相同的两个456B缓冲区一样。内循环分支会在每个外循环中误预测一次(当不离开循环时)。(总时间较长,因为计算机没有完全处于空闲状态。可能另一个逻辑核心在某些时间处于活动状态,并且在中断上花费了更多时间(因为用户空间测量的频率远低于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微指令(或者其中一些是2 uop指令时发出6微指令,例如Ryzen上的AVX 256b东西)。我不确定它的前端将如何处理stosd
,这是Ryzen(与Intel相同)上的3微秒。我认为内循环中的其他指令与Skylake和所有单联指令的延迟相同。(包括adc eax, [edi+edx]
,这是优于Skylake的优势)。
如果我将数字存储为每个字节1个小数位数,则可能会小很多,但可能慢9倍。用产生进位cmp
和用进行调整cmov
的效果相同,但是做1/9的工作。每字节2个十进制数字(以100为基数,不是4比特的BCD带有慢速DAA
)也可以,并且div r8
/ 将按add ax, 0x3030
打印顺序将0-99字节转换为两个ASCII数字。但是,每个字节完全不需要1位数字div
,只需循环并添加0x30。如果我按打印顺序存储字节,那将使第二个循环真的很简单。
每个64位整数使用18或19个十进制数字(在64位模式下)将使其运行速度快大约两倍,但对于所有REX前缀和64位常量而言,其代码大小成本都很高。64位模式下的32位肢体禁止使用pop eax
代替lodsd
。我仍然可以通过将其esp
用作非指针暂存寄存器(交换esi
and 的用法esp
),而不是r8d
用作第8个寄存器来避免REX前缀。
如果制作的是可调用函数版本,则转换为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倍(对于100M外循环的迭代,预测误差为128M倍)。 114
每个内部循环只被一次错误地预测,与我在斐波那契循环中发现的一样。
我很好奇并尝试了一下,将内循环以6展开%rep 6
(因为它平均分配了114)。多数情况下消除了分支遗漏。我将edx
负值用作mov
商店的抵消额,因此adc eax,[edi]
可以保持微融合。(所以我可以避免stosd
)。我将lea
update edi
拖出了该%rep
块,因此每6个存储只执行一次指针更新。
我也摆脱了外循环中的所有部分寄存器的内容,尽管我认为这并不重要。使外部循环末端的CF不依赖于最终ADC可能会有所帮助,因此可以开始一些内部循环的操作。外循环代码可能会进行更多优化,这neg edx
是我做的最后一件事xchg
,仅用2 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
计数。这意味着它们中的许多是微融合的:在融合域(issue / ROB)中为1 uop,而在非融合域(调度器/执行单元)中为2 uop。并且在发布/重命名阶段中消除了极少的内容(例如mov
寄存器复制或xor
-zeroing,它们需要发布但不需要执行单元)。消除了的微词会反过来使计数不平衡。
branch-misses
从1G下降到了约40万,所以展开工作了。 resource_stalls.any
现在意义重大,这意味着前端不再是瓶颈:相反,后端落后了并限制了前端。 idq_uops_not_delivered.core
仅计算前端未传递uops,但后端没有停止的周期。不错,很低,表明前端瓶颈很少。
有趣的事实:python版本花费其一半以上的时间除以10而不是相加。(更换a/=10
用a>>=64
其加速超过2倍,但改变的结果,因为二进制截断!=小数截断。)
我的asm版本当然针对此问题大小进行了优化,并使用了硬编码的循环迭代次数。即使移位任意精度的数字也会复制它,但是我的版本可以从偏移量中读取,以便在接下来的两次迭代中跳过。
我介绍了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
(parens)中的数字是对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.
记忆力呢?