Mã máy x86 32 bit (với các cuộc gọi hệ thống Linux): 106 105 byte
changelog: đã lưu một byte trong phiên bản nhanh vì hằng số tắt không thay đổi kết quả cho Fib (1G).
Hoặc 102 byte cho phiên bản chậm hơn 18% (trên Skylake) (sử dụng mov
/ sub
/ cmc
thay vì lea
/ cmp
trong vòng lặp bên trong, để tạo ra thực hiện và gói 10**9
thay vì 2**32
). Hoặc 101 byte cho phiên bản chậm hơn ~ 5,3 lần với một nhánh trong xử lý mang theo trong vòng lặp bên trong nhất. (Tôi đã đo tỷ lệ dự đoán sai 25,4% chi nhánh!)
Hoặc 104/101 byte nếu số 0 đứng đầu được cho phép. (Phải mất thêm 1 byte để mã cứng bỏ qua 1 chữ số đầu ra, đây là điều cần thiết cho Fib (10 ** 9)).
Thật không may, chế độ NASM của TIO dường như bỏ qua -felf32
trong các cờ biên dịch. Dù sao đây cũng là một liên kết với mã nguồn đầy đủ của tôi, với tất cả các ý tưởng thử nghiệm trong các bình luận.
Đây là một chương trình hoàn chỉnh . Nó in 1000 chữ số đầu tiên của Fib (10 ** 9) theo sau là một số chữ số phụ (một vài chữ số cuối bị sai) theo sau là một số byte rác (không bao gồm một dòng mới). Hầu hết rác không phải là ASCII, vì vậy bạn có thể muốn chuyển qua cat -v
. Tuy nhiên, nó không phá vỡ trình giả lập thiết bị đầu cuối của tôi (KDE konsole
). "Các byte rác" đang lưu trữ Fib (999999999). Tôi đã có -1024
trong một đăng ký, vì vậy việc in 1024 byte rẻ hơn so với kích thước phù hợp.
Tôi chỉ đếm mã máy (kích thước của đoạn văn bản của tệp thực thi tĩnh của tôi), chứ không phải là lông tơ làm cho nó có thể thực thi được ELF. ( Có thể thực thi ELF rất nhỏ , nhưng tôi không muốn bận tâm đến điều đó). Hóa ra là ngắn hơn để sử dụng bộ nhớ ngăn xếp thay vì BSS, vì vậy tôi có thể biện minh rằng không tính bất cứ điều gì khác trong nhị phân vì tôi không phụ thuộc vào bất kỳ siêu dữ liệu nào. (Sản xuất một nhị phân tĩnh bị tước theo cách thông thường làm cho ELF 340 byte có thể thực thi được.)
Bạn có thể tạo một hàm từ mã này mà bạn có thể gọi từ C. Nó sẽ tốn một vài byte để lưu / khôi phục con trỏ ngăn xếp (có thể trong một thanh ghi MMX) và một số chi phí khác, nhưng cũng lưu byte bằng cách quay lại chuỗi trong bộ nhớ, thay vì thực hiện một write(1,buf,len)
cuộc gọi hệ thống. Tôi nghĩ rằng chơi golf bằng mã máy sẽ giúp tôi có được một chút chùng ở đây, vì không ai khác thậm chí đã đăng một câu trả lời bằng bất kỳ ngôn ngữ nào mà không có độ chính xác mở rộng, nhưng tôi nghĩ rằng một phiên bản chức năng của điều này vẫn phải dưới 120 byte mà không phải chơi lại toàn bộ Điều.
Thuật toán:
lực lượng vũ phu a+=b; swap(a,b)
, cắt ngắn khi cần thiết để chỉ giữ lại hàng đầu> = 1017 chữ số thập phân. Nó chạy trong 1 phút13 trên máy tính của tôi (hoặc 322,47 tỷ chu kỳ đồng hồ + - 0,05%) (và có thể nhanh hơn vài% với một vài byte kích thước mã bổ sung, hoặc giảm xuống còn 62 giây với kích thước mã lớn hơn nhiều từ việc hủy vòng lặp. toán thông minh, chỉ làm cùng một công việc với ít chi phí). Nó dựa trên triển khai Python của @ AndersKaseorg , chạy trong 12 phút35 trên máy tính của tôi (Skylake i7-6700k 4,4 GHz). Cả hai phiên bản đều không có bất kỳ lỗi bộ nhớ cache L1D nào, vì vậy DDR4-2666 của tôi không thành vấn đề.
Không giống như Python, tôi lưu trữ các số có độ chính xác mở rộng theo định dạng giúp cắt bớt các chữ số thập phân miễn phí . Tôi lưu trữ các nhóm gồm 9 chữ số thập phân trên mỗi số nguyên 32 bit, do đó, một con trỏ bù trừ 9 chữ số thấp. Đây thực sự là cơ sở 1 tỷ, có sức mạnh bằng 10. (Điều trùng hợp hoàn toàn là thử thách này cần số Fibonacci 1 tỷ, nhưng nó giúp tôi tiết kiệm được một vài byte so với hai hằng số riêng biệt.)
Theo thuật ngữ của GMP , mỗi đoạn 32 bit của một số có độ chính xác mở rộng được gọi là "chi". Thực hiện trong khi thêm phải được tạo thủ công với so sánh với 1e9, nhưng sau đó được sử dụng bình thường làm đầu vào cho ADC
hướng dẫn thông thường cho chi tiếp theo. (Tôi cũng phải tự bọc theo [0..999999999]
phạm vi, thay vì ở 2 ^ 32 ~ = 4.295e9. Tôi làm điều này không phân nhánh với lea
+ cmov
, sử dụng kết quả thực hiện từ so sánh.)
Khi chi cuối cùng tạo ra giá trị khác không, hai lần lặp tiếp theo của vòng lặp bên ngoài đọc từ 1 chi cao hơn bình thường, nhưng vẫn ghi vào cùng một vị trí. Điều này giống như thực hiện memcpy(a, a+4, 114*4)
chuyển sang phải bằng 1 chi, nhưng được thực hiện như một phần của hai vòng bổ sung tiếp theo. Điều này xảy ra cứ sau 18 lần lặp.
Hacks để tiết kiệm kích thước và hiệu suất:
Những thứ thông thường như lea ebx, [eax-4 + 1]
thay vì mov ebx, 1
, khi tôi biết điều đó eax=4
. Và sử dụng loop
ở những nơi LOOP
chậm chạp chỉ có tác động nhỏ.
Cắt ngắn 1 chi miễn phí bằng cách bù đắp các con trỏ mà chúng ta đọc được, trong khi vẫn ghi vào đầu bộ đệm trong adc
vòng lặp bên trong. Chúng tôi đọc từ [edi+edx]
và viết thư cho [edi]
. Vì vậy, chúng ta có thể nhận được edx=0
hoặc 4
để có được một phần bù đọc-ghi cho đích. Chúng ta cần làm điều này trong 2 lần lặp liên tiếp, đầu tiên là bù đắp cả hai, sau đó chỉ bù đắp cho dst. Chúng tôi phát hiện trường hợp thứ 2 bằng cách xem xét esp&4
trước khi đặt lại các con trỏ về phía trước bộ đệm (sử dụng &= -1024
, vì các bộ đệm được căn chỉnh). Xem ý kiến trong mã.
Môi trường khởi động quy trình Linux (đối với thực thi tĩnh) không có hầu hết các thanh ghi và bộ nhớ ngăn xếp bên dưới esp
/ rsp
không có giá trị. Chương trình của tôi tận dụng điều này. Trong phiên bản chức năng có thể gọi được của điều này (trong đó ngăn xếp chưa được phân bổ có thể bị bẩn), tôi có thể sử dụng BSS cho bộ nhớ 0 (với chi phí có thể hơn 4 byte để thiết lập con trỏ). Zeroing edx
sẽ mất 2 byte. Hệ thống x86-64 V ABI không đảm bảo một trong hai điều này, nhưng việc triển khai Linux của nó không có gì (để tránh rò rỉ thông tin ra khỏi kernel). Trong một quy trình được liên kết động, /lib/ld.so
chạy trước _start
và không để các thanh ghi khác không (và có thể là rác trong bộ nhớ bên dưới con trỏ ngăn xếp).
Tôi giữ -1024
trong ebx
để sử dụng bên ngoài của vòng. Sử dụng bl
như một bộ đếm cho các vòng lặp bên trong, kết thúc bằng 0 (là byte thấp của -1024
, do đó khôi phục hằng số để sử dụng bên ngoài vòng lặp). Intel Haswell và sau đó không có hình phạt hợp nhất đăng ký một phần cho các thanh ghi low8 (và thực tế thậm chí không đổi tên chúng một cách riêng biệt) , do đó, có sự phụ thuộc vào đăng ký đầy đủ, như trên AMD (không phải là vấn đề ở đây). Điều này sẽ là khủng khiếp trên Nehalem và trước đó, có các quầy đăng ký một phần khi sáp nhập. Có những nơi khác tôi viết regs một phần và sau đó đọc toàn bộ reg mà không cần xor
-zerending hoặc amovzx
, thông thường bởi vì tôi biết rằng một số mã trước đó đã loại bỏ các byte trên và một lần nữa điều đó tốt cho gia đình AMD và Intel SnB, nhưng chậm trên Intel trước Sandybridge.
Tôi sử dụng 1024
làm số byte để ghi vào stdout ( sub edx, ebx
), vì vậy chương trình của tôi in một số byte rác sau các chữ số Fibonacci, vì mov edx, 1000
chi phí nhiều byte hơn.
(không được sử dụng) adc ebx,ebx
với EBX = 0 để nhận EBX = CF, tiết kiệm 1 byte so với setc bl
.
dec
/ jnz
bên trong một adc
vòng lặp bảo tồn CF mà không gây ra tình trạng treo cờ một phần khi adc
đọc cờ trên Intel Sandybridge và sau đó. Nó không tốt cho các CPU trước đó , nhưng AFAIK miễn phí trên Skylake. Hoặc tồi tệ nhất, một uop thêm.
Sử dụng bộ nhớ dưới đây esp
như một vùng màu đỏ khổng lồ . Vì đây là một chương trình Linux hoàn chỉnh, tôi biết rằng tôi đã không cài đặt bất kỳ trình xử lý tín hiệu nào và sẽ không có gì khác ngăn chặn bộ nhớ ngăn xếp không gian người dùng một cách không đồng bộ. Đây có thể không phải là trường hợp trên các hệ điều hành khác.
Tận dụng công cụ ngăn xếp để tiết kiệm băng thông phát hành uop bằng cách sử dụng pop eax
(1 uop + uop đồng bộ hóa ngăn xếp thường xuyên) thay vì lodsd
(2 uops trên Haswell / Skylake, 3 trên IvB và trước đó theo bảng hướng dẫn của Agner Fog )). IIRC, điều này đã giảm thời gian chạy từ khoảng 83 giây xuống 73. Tôi có thể có cùng tốc độ từ việc sử dụng mov
chế độ địa chỉ được lập chỉ mục, giống như mov eax, [edi+ebp]
nơi ebp
giữ độ lệch giữa bộ đệm src và dst. (Nó sẽ làm cho mã bên ngoài vòng lặp bên trong trở nên phức tạp hơn, phải phủ nhận thanh ghi bù như là một phần của việc hoán đổi src và dst cho các lần lặp Fibonacci.) Xem phần "hiệu suất" bên dưới để biết thêm.
bắt đầu chuỗi bằng cách lặp lại lần đầu tiên (một byte stc
), thay vì lưu trữ 1
trong bộ nhớ ở bất cứ đâu. Rất nhiều vấn đề cụ thể khác được ghi lại trong các bình luận.
Danh sách NASM (mã máy + nguồn) , được tạo bằng nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Sau đó, tôi đã gỡ bỏ một số khối nội dung nhận xét, do đó, việc đánh số dòng có khoảng trống.) Để loại bỏ các cột hàng đầu để bạn có thể đưa nó vào YASM hoặc NASM, hãy sử dụng 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
Có lẽ có chỗ để chơi golf thêm vài byte trong số này, nhưng tôi đã dành ít nhất 12 giờ cho việc này hơn 2 ngày. Tôi không muốn hy sinh tốc độ, mặc dù nó đủ nhanh hơn và có chỗ để làm cho nó nhỏ hơn theo cách tốn chi phí . Một phần lý do của tôi để đăng là cho thấy tôi có thể tạo phiên bản asm brute-force nhanh như thế nào. Nếu bất cứ ai muốn thực sự đi cho kích thước tối thiểu nhưng có thể chậm hơn 10 lần (ví dụ 1 chữ số trên mỗi byte), vui lòng sao chép này làm điểm bắt đầu.
Kết quả thực thi (từ yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) là 340B (tước):
size fibonacci-1G
text data bss dec hex filename
105 0 0 105 69 fibonacci-1G
Hiệu suất
adc
Vòng lặp bên trong là 10 uop miền hợp nhất trên Skylake (uop đồng bộ hóa +1 mỗi ~ 128 byte), do đó, nó có thể phát hành ở một chu kỳ trên 2,5 chu kỳ trên Skylake với thông lượng đầu cuối tối ưu (bỏ qua các uops đồng bộ hóa ngăn xếp) . Độ trễ của đường dẫn quan trọng là 2 chu kỳ, đối với chuỗi phụ thuộc vòng lặp mang theo vòng lặp adc
-> cmp
-> vòng lặp tiếp theo adc
, do đó, nút cổ chai phải là giới hạn sự cố phía trước ~ 2,5 chu kỳ trên mỗi lần lặp.
adc eax, [edi + edx]
là 2 uops miền không sử dụng cho các cổng thực thi: load + ALU. Nó vi cầu chì trong bộ giải mã (1 uop miền hợp nhất), nhưng không ghép nối trong giai đoạn phát hành thành 2 uops miền hợp nhất, vì chế độ đánh địa chỉ được lập chỉ mục, ngay cả trên Haswell / Skylake . Tôi nghĩ rằng nó sẽ duy trì trạng thái hợp nhất, giống như add eax, [edi + edx]
vậy, nhưng có lẽ việc giữ các chế độ địa chỉ được lập chỉ mục mà micro-fuse không hoạt động đối với các uops đã có 3 đầu vào (cờ, bộ nhớ và đích). Khi tôi viết nó, tôi đã nghĩ rằng nó sẽ không có nhược điểm về hiệu suất, nhưng tôi đã sai. Cách xử lý cắt ngắn này làm chậm vòng lặp bên trong mỗi lần, cho dù edx
là 0 hay 4.
Sẽ nhanh hơn để xử lý bù đọc-ghi cho dst bằng cách bù đắp edi
và sử dụng edx
để điều chỉnh cửa hàng. Vì vậy, adc eax, [edi]
/ ... / mov [edi+edx], eax
/ lea edi, [edi+4]
thay vì stosd
. Haswell và sau này có thể giữ một cửa hàng được lập chỉ mục micro-fuse. (Sandybridge / IvB cũng sẽ kiểm tra nó.)
Trên Intel Haswell và trước đó, adc
và cmovc
là 2 uops, với độ trễ 2c . ( adc eax, [edi+edx]
vẫn chưa được dán nhiều lớp trên Haswell và phát hành dưới dạng 3 uops miền hợp nhất). Broadwell và sau đó cho phép các vòng 3 đầu vào không chỉ là FMA (Haswell), thực hiện adc
và cmovc
(và một vài thứ khác) các hướng dẫn đơn lẻ, giống như chúng đã có trên AMD trong một thời gian dài. (Đây là một lý do AMD đã thực hiện tốt các tiêu chuẩn chuẩn độ mở rộng trong một thời gian dài.) iter trường hợp tốt nhất, bỏ qua các uops stack-sync.
Sử dụng pop
mà không có sự cân bằng push
bên trong một vòng lặp có nghĩa là vòng lặp không thể chạy từ LSD (trình phát luồng vòng lặp) và phải được đọc lại từ bộ đệm uop vào IDQ mỗi lần. Nếu bất cứ điều gì, đó là một điều tốt trên Skylake, vì vòng lặp 9 hoặc 10 uop không hoàn toàn tối ưu hóa ở 4 vòng mỗi chu kỳ . Đây có lẽ là một phần lý do tại sao thay thế lodsd
bằng pop
giúp rất nhiều. (LSD không thể khóa các vòng lặp vì điều đó sẽ không còn chỗ để chèn một uop đồng bộ hóa ngăn xếp .) (BTW, một bản cập nhật vi mã vô hiệu hóa hoàn toàn LSD trên Skylake và Skylake-X để sửa lỗi. Tôi đã đo ở trên trước khi nhận được bản cập nhật đó.)
Tôi đã định hình nó trên Haswell và thấy rằng nó chạy trong 380,31 tỷ chu kỳ xung nhịp (bất kể tần số CPU, vì nó chỉ sử dụng bộ đệm L1D, không phải bộ nhớ). Thông lượng vấn đề mặt trước là 3,72 uops miền hợp nhất trên mỗi đồng hồ, so với 3,70 đối với Skylake. (Nhưng tất nhiên hướng dẫn trên mỗi chu kỳ đã giảm xuống 2,42 từ 2,87, bởi vì adc
và cmov
là 2 uops trên Haswell.)
push
để thay thế stosd
có lẽ sẽ không giúp được gì nhiều, bởi vì adc [esp + edx]
sẽ kích hoạt một uop đồng bộ hóa ngăn xếp mỗi lần. Và sẽ có chi phí một byte cho std
nên lodsd
đi theo một hướng khác. ( mov [edi], eax
/ lea edi, [edi+4]
để thay thế stosd
là một chiến thắng, đi từ 32.909 Xe máy cho 100 triệu iters đến 31.954 Xe máy cho 100 triệu iters. Có vẻ như stosd
giải mã là 3 uops, với các uops địa chỉ cửa hàng / dữ liệu lưu trữ không được hợp nhất, do đó push
+ đồng bộ hóa ngăn xếp uops vẫn có thể nhanh hơn stosd
)
Hiệu suất thực tế của ~ 322,47 tỷ chu kỳ cho các lần lặp 1G của 114 chi hoạt động với 2.824 chu kỳ trên mỗi lần lặp của vòng lặp bên trong , cho phiên bản 105B nhanh trên Skylake. (Xem ocperf.py
đầu ra bên dưới). Điều đó chậm hơn tôi dự đoán từ phân tích tĩnh, nhưng tôi đã bỏ qua chi phí hoạt động của vòng lặp ngoài và bất kỳ vòng lặp đồng bộ hóa nào.
Perf quầy cho branches
và branch-misses
cho thấy rằng mispredicts vòng lặp bên trong một lần mỗi vòng ngoài (trên lặp cuối cùng, khi nó không được thực hiện). Điều đó cũng chiếm một phần của thời gian thêm.
Tôi có thể lưu kích thước mã bằng cách tạo vòng lặp bên trong nhất có độ trễ 3 chu kỳ cho đường dẫn quan trọng, sử dụng mov esi,eax
/ sub eax,ebp
/ cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) thay vì lea esi, [eax - 1000000000]
/ cmp ebp,eax
/ cmovc
(6 + 2 + 3 = 11B ). Các cmov
/ stosd
là ra khỏi con đường quan trọng. (Công cụ tăng dần stosd
có thể chạy tách biệt khỏi cửa hàng, do đó, mỗi lần lặp sẽ loại bỏ một chuỗi phụ thuộc ngắn.) Nó đã sử dụng để tiết kiệm 1B khác bằng cách thay đổi hướng dẫn ebp init từ lea ebp, [ecx-1]
sang mov ebp,eax
, nhưng tôi phát hiện ra rằng đã saiebp
không thay đổi kết quả. Điều này sẽ cho phép một chi chính xác == 1000000000 thay vì quấn và tạo ra một vật mang, nhưng lỗi này lan truyền chậm hơn so với chúng tôi Fib () phát triển, vì vậy điều này xảy ra không làm thay đổi các chữ số 1k hàng đầu của kết quả cuối cùng. Ngoài ra, tôi nghĩ rằng lỗi đó có thể tự sửa khi chúng ta chỉ cần thêm, vì có chỗ trong một chi để giữ nó mà không bị tràn. Ngay cả 1G + 1G cũng không vượt quá số nguyên 32 bit, do đó, cuối cùng nó sẽ thấm dần lên hoặc bị cắt bớt.
Phiên bản độ trễ 3c là 1 uop thêm, do đó, front-end có thể phát hành nó với tốc độ một trên 2,75c trên Skylake, chỉ nhanh hơn một chút so với back-end có thể chạy nó. (Trên Haswell, nó sẽ có tổng số 13 uops vì nó vẫn sử dụng adc
và cmov
, và nút cổ chai ở mặt trước ở mức 3,25c mỗi lần lặp).
Trong thực tế, nó chạy hệ số chậm hơn 1,18 trên Skylake (3,34 chu kỳ trên một chi), thay vì 3 / 2,5 = 1,2 mà tôi dự đoán để thay thế nút cổ chai phía trước bằng nút cổ chai trễ khi chỉ nhìn vào vòng lặp bên trong mà không đồng bộ hóa ngăn xếp ôi. Vì các nút đồng bộ hóa ngăn xếp chỉ làm tổn thương phiên bản nhanh (bị tắc nghẽn ở mặt trước thay vì độ trễ), nên không cần phải giải thích nhiều về nó. ví dụ 3 / 2.54 = 1.18.
Một yếu tố khác là phiên bản độ trễ 3c có thể phát hiện ra dự đoán sai khi rời khỏi vòng lặp bên trong trong khi đường dẫn quan trọng vẫn đang thực thi (vì giao diện người dùng có thể vượt lên phía trước, để cho việc thực hiện không theo thứ tự chạy vòng lặp- truy cập uops), vì vậy hình phạt dự đoán sai hiệu quả là thấp hơn. Mất các chu kỳ front-end cho phép back-end bắt kịp.
Nếu không phải như vậy, chúng ta có thể tăng tốc cmc
phiên bản 3c bằng cách sử dụng một nhánh ở vòng ngoài thay vì xử lý không phân nhánh của carryDef -> edx và đặc biệt. Dự đoán chi nhánh + thực thi đầu cơ cho một phụ thuộc điều khiển thay vì phụ thuộc dữ liệu có thể cho phép lần lặp tiếp theo bắt đầu chạy adc
vòng lặp trong khi các vòng lặp từ vòng lặp bên trong trước đó vẫn đang hoạt động. Trong phiên bản không phân nhánh, các địa chỉ tải trong vòng lặp bên trong có sự phụ thuộc dữ liệu vào CF từ chi cuối cùng adc
của chi cuối cùng.
Các nút thắt phiên bản bên trong độ trễ 2c ở mặt trước, do đó, back-end khá nhiều theo kịp. Nếu mã vòng ngoài có độ trễ cao, giao diện người dùng có thể nhận được các lệnh phát hành trước từ lần lặp tiếp theo của vòng lặp bên trong. (Nhưng trong trường hợp này, công cụ vòng ngoài có nhiều ILP và không có công cụ có độ trễ cao, do đó, back-end không bắt kịp để làm gì khi nó bắt đầu nhai qua các vòng trong trình lập lịch không theo thứ tự như đầu vào của họ trở nên sẵn sàng).
### 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 %)
là độ lệch chuẩn trong 4 lần chạy cho số đó. Thật thú vị khi nó chạy một số lượng lớn các hướng dẫn. 924 tỷ đó không phải là ngẫu nhiên. Tôi đoán rằng vòng lặp bên ngoài chạy tổng cộng 924 hướng dẫn.
uops_issued
là số lượng miền hợp nhất (có liên quan đến băng thông của sự cố phía trước), trong khi đó uops_executed
là số lượng miền không được sử dụng (số lượng uops được gửi đến các cổng thực thi). Micro-fusion gói 2 uop miền không sử dụng vào một uop miền hợp nhất, nhưng loại bỏ Mov có nghĩa là một số uops miền hợp nhất không cần bất kỳ cổng thực thi nào. Xem câu hỏi được liên kết để biết thêm về cách đếm uops và miền hợp nhất với miền không được sử dụng. (Cũng xem bảng hướng dẫn của Agner Fog và hướng dẫn uarch và các liên kết hữu ích khác trong wiki thẻ SO x86 ).
Từ một lần chạy khác, đo lường những thứ khác nhau: bộ nhớ cache L1D hoàn toàn không đáng kể, như mong đợi để đọc / ghi hai bộ đệm 456B giống nhau. Nhánh vòng lặp bên trong đánh giá sai một lần cho mỗi vòng lặp bên ngoài (khi nó không được thực hiện để rời khỏi vòng lặp). (Tổng thời gian cao hơn vì máy tính không hoàn toàn nhàn rỗi. Có lẽ lõi logic khác đã hoạt động một thời gian và nhiều thời gian hơn đã bị gián đoạn (vì tần số đo không gian của người dùng ở xa hơn 4.400GHz). Hoặc nhiều lõi đã hoạt động nhiều hơn trong thời gian ngắn, làm giảm tối đa turbo. Tôi không theo dõi cpu_clk_unhalted.one_thread_active
để xem liệu cạnh tranh HT có phải là một vấn đề không.)
### 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
Mã của tôi có thể chạy tốt hơn trong các chu kỳ ít hơn trên Ryzen, có thể phát hành 5 vòng trên mỗi chu kỳ (hoặc 6 khi một số trong số chúng là các hướng dẫn 2 uop, như công cụ AVX 256b trên Ryzen). Tôi không chắc chắn phần đầu của nó sẽ làm gì stosd
, đó là 3 uops trên Ryzen (giống như Intel). Tôi nghĩ rằng các hướng dẫn khác trong vòng lặp bên trong có cùng độ trễ như Skylake và tất cả các uop đơn. (Bao gồm adc eax, [edi+edx]
, đó là một lợi thế so với Skylake).
Điều này có thể nhỏ hơn đáng kể, nhưng có thể chậm hơn 9 lần, nếu tôi lưu trữ các số dưới dạng 1 chữ số thập phân trên mỗi byte . Tạo ra mang đi cmp
và điều chỉnh với cmov
sẽ hoạt động như nhau, nhưng thực hiện 1/9 công việc. 2 chữ số thập phân trên mỗi byte (cơ sở 100, không phải BCD 4 bit chậmDAA
) cũng sẽ hoạt động và div r8
/ add ax, 0x3030
biến 0-99 byte thành hai chữ số ASCII theo thứ tự in. Nhưng 1 chữ số trên mỗi byte hoàn toàn không cần div
, chỉ cần lặp và thêm 0x30. Nếu tôi lưu trữ các byte theo thứ tự in, điều đó sẽ làm cho vòng lặp thứ 2 thực sự đơn giản.
Sử dụng 18 hoặc 19 chữ số thập phân cho mỗi số nguyên 64 bit (ở chế độ 64 bit) sẽ giúp nó chạy nhanh gấp đôi, nhưng chi phí kích thước mã đáng kể cho tất cả các tiền tố REX và cho các hằng 64 bit. Tay chân 32 bit ở chế độ 64 bit ngăn sử dụng pop eax
thay vì lodsd
. Tôi vẫn có thể tránh các tiền tố REX bằng cách sử dụng esp
như một thanh ghi cào không con trỏ (hoán đổi cách sử dụng esi
và esp
), thay vì sử dụng r8d
như một thanh ghi thứ 8.
Nếu tạo phiên bản chức năng có thể gọi được, chuyển đổi thành 64 bit và sử dụng r8d
có thể rẻ hơn so với lưu / khôi phục rsp
. 64 bit cũng không thể sử dụng dec r32
mã hóa một byte (vì đó là tiền tố REX). Nhưng chủ yếu tôi đã kết thúc bằng dec bl
2 byte. (Bởi vì tôi có một hằng số ở các byte trên ebx
và chỉ sử dụng nó bên ngoài các vòng lặp bên trong, hoạt động vì byte thấp của hằng số là 0x00
.)
Phiên bản hiệu suất cao
Để có hiệu suất tối đa (không phải là môn đánh gôn), bạn muốn hủy kiểm soát vòng lặp bên trong để nó chạy tối đa 22 lần lặp, đây là mô hình được thực hiện / không thực hiện đủ ngắn để các công cụ dự đoán nhánh hoạt động tốt. Trong các thí nghiệm của tôi, mov cl, 22
trước một .inner: dec cl/jnz .inner
vòng lặp có rất ít sai sót (như 0,05%, ít hơn một lần cho mỗi lần chạy hoàn toàn của vòng lặp bên trong), nhưng lại đánh giá mov cl,23
sai từ 0,35 đến 0,6 lần cho mỗi vòng lặp bên trong. 46
là đặc biệt xấu, dự đoán sai ~ 1,28 lần cho mỗi vòng lặp bên trong (128M lần cho các vòng lặp vòng ngoài 100M). 114
dự đoán sai chính xác một lần cho mỗi vòng lặp bên trong, giống như tôi đã tìm thấy như là một phần của vòng lặp Fibonacci.
Tôi đã tò mò và thử nó, bỏ vòng lặp bên trong bằng 6 %rep 6
(vì nó chia đều 114). Điều đó chủ yếu loại bỏ các chi nhánh bỏ lỡ. Tôi đã làm edx
tiêu cực và sử dụng nó như một sự bù đắp cho mov
các cửa hàng, vì vậy adc eax,[edi]
có thể vẫn còn hợp nhất. (Và vì vậy tôi có thể tránh stosd
). Tôi đã kéo lea
bản cập nhật edi
ra khỏi %rep
khối, vì vậy nó chỉ thực hiện một cập nhật con trỏ trên 6 cửa hàng.
Tôi cũng đã loại bỏ tất cả những thứ đăng ký một phần ở vòng ngoài, mặc dù tôi không nghĩ đó là điều quan trọng. Nó có thể giúp một chút để có CF ở cuối vòng ngoài không phụ thuộc vào ADC cuối cùng, vì vậy một số vòng lặp trong vòng có thể bắt đầu. Mã vòng ngoài có lẽ có thể được tối ưu hóa hơn một chút, vì đó neg edx
là điều cuối cùng tôi đã làm, sau khi thay thế xchg
chỉ bằng 2 mov
hướng dẫn (vì tôi vẫn còn 1) và sắp xếp lại chuỗi dep cùng với việc thả 8 bit đăng ký công cụ.
Đây là nguồn NASM của vòng lặp Fibonacci. Đây là phần thay thế thả xuống cho phần đó của phiên bản gốc.
;;;; 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
Hiệu suất:
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% )
Đó là cho cùng một Fib (1G), tạo ra cùng một đầu ra trong 62,3 giây thay vì 73 giây. (273.146G chu kỳ, so với 322.467G. Vì mọi thứ đều đạt được trong bộ đệm L1, chu kỳ xung nhịp lõi thực sự là tất cả những gì chúng ta cần xem xét.)
Lưu ý tổng số thấp hơn nhiều, thấp hơn nhiều uops_issued
so với uops_executed
số lượng. Điều đó có nghĩa là nhiều trong số chúng đã được hợp nhất vi mô: 1 uop trong miền được hợp nhất (vấn đề / ROB), nhưng 2 uops trong miền không được sử dụng (đơn vị lập lịch / thực thi)). Và số ít đó đã bị loại bỏ trong giai đoạn phát hành / đổi tên (như mov
sao chép đăng ký, hoặc xor
xử lý, cần phát hành nhưng không cần đơn vị thực thi). Uops bị loại bỏ sẽ làm mất cân bằng cách khác.
branch-misses
giảm xuống ~ 400k, từ 1G, do đó, unrolling hoạt động. resource_stalls.any
bây giờ rất quan trọng, điều đó có nghĩa là front-end không còn là nút cổ chai nữa: thay vào đó, back-end đang ở phía sau và giới hạn front-end. idq_uops_not_delivered.core
chỉ tính các chu kỳ trong đó mặt trước không cung cấp các vòng, nhưng mặt sau không bị đình trệ. Đó là tốt đẹp và thấp, chỉ ra vài nút thắt phía trước.
Sự thật thú vị: phiên bản python dành hơn một nửa thời gian chia cho 10 thay vì thêm. (Thay thế a/=10
bằng a>>=64
tốc độ tăng hơn 2 lần, nhưng thay đổi kết quả vì cắt nhị phân! = Cắt thập phân.)
Phiên bản asm của tôi tất nhiên được tối ưu hóa đặc biệt cho kích thước vấn đề này, với số lần lặp lặp được tính mã hóa cứng. Ngay cả việc thay đổi một số chính xác tùy ý sẽ sao chép nó, nhưng phiên bản của tôi chỉ có thể đọc từ một phần bù cho hai lần lặp tiếp theo để bỏ qua điều đó.
Tôi đã định hình phiên bản python (python2.7 64 bit trên 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
Các số trong (parens) là bao nhiêu thời gian mà bộ đếm hoàn hảo đã được lấy mẫu. Khi nhìn vào nhiều quầy hơn so với hỗ trợ CTNH, sự hoàn hảo xoay giữa các quầy khác nhau và ngoại suy. Điều đó hoàn toàn tốt cho một nhiệm vụ dài.
Nếu tôi chạy perf
sau khi thiết lập sysctl kernel.perf_event_paranoid = 0
(hoặc chạy perf
bằng root), nó sẽ đo 4.400GHz
. cycles:u
không tính thời gian bị gián đoạn (hoặc cuộc gọi hệ thống), chỉ có chu kỳ không gian người dùng. Máy tính để bàn của tôi gần như hoàn toàn nhàn rỗi, nhưng đây là điển hình.
Your program must be fast enough for you to run it and verify its correctness.
Còn trí nhớ thì sao?