Nếu bạn không cần tính ngẫu nhiên chất lượng rất cao và phân phối gần đồng đều đủ tốt, bạn có thể thực hiện rất nhanh, đặc biệt là trên CPU hiện đại có vectơ số nguyên SIMD hiệu quả như x86 với SSE2 hoặc AVX2.
Điều này giống như câu trả lời của @ NominalAnimal vì cả hai chúng tôi đều có cùng một ý tưởng, nhưng được vector hóa thủ công cho x86. (Và với các số ngẫu nhiên có chất lượng kém hơn, nhưng vẫn có thể đủ tốt cho nhiều trường hợp sử dụng.) Điều này chạy nhanh hơn khoảng 15 hoặc 30 lần so với mã của @ Nominal, ở mức ~ 13GB / giây đầu ra ASCII trên Intel Haswell 2,5 GHz CPU với AVX2. Con số đó vẫn thấp hơn băng thông bộ nhớ chính tối đa theo lý thuyết (DDR3-1600 kênh đôi khoảng 25,6GB / giây), nhưng tôi đã định thời gian ghi thành / dev / null để nó thực sự chỉ viết lại bộ đệm vẫn còn nóng trong bộ đệm. Skylake nên chạy cùng mã này nhanh hơn đáng kể so với Haswell (xem phần dưới của câu trả lời này).
Giả sử bạn thực sự tắc nghẽn trên I / O vào đĩa hoặc đường ống này ở đâu đó, việc triển khai nhanh có nghĩa là CPU của bạn thậm chí không phải xung nhịp cao hơn không hoạt động. Nó sử dụng tổng năng lượng ít hơn nhiều để tạo ra kết quả. (Tuổi thọ pin / nhiệt / nóng lên toàn cầu.)
Tốc độ này nhanh đến mức bạn có thể không muốn ghi nó vào đĩa. Chỉ cần tạo lại khi cần thiết (từ cùng một hạt giống nếu bạn muốn lại cùng một dữ liệu). Ngay cả khi bạn muốn đưa nó vào một quy trình đa luồng có thể sử dụng tất cả các CPU, thì việc chạy này để dẫn dữ liệu đến nó sẽ khiến nó nóng lên trong bộ đệm L3 (và bộ đệm L2 trên lõi đã viết nó) và sử dụng rất nhiều thời gian CPU ít. (Nhưng lưu ý rằng đường ống thêm rất nhiều chi phí so với ghi vào /dev/null
. Trên Skylake i7-6700k, đường ống đến wc -c
hoặc một chương trình khác chỉ đọc + loại bỏ đầu vào của nó, nó chậm hơn khoảng 8 lần so với ghi/dev/null
và chỉ sử dụng 70% CPU. Nhưng đó vẫn là 4.0GB / giây trên CPU 3.9GHz.
Việc tạo lại nó nhanh hơn đọc lại ngay cả từ SSD kết nối PCIe nhanh, nhưng IDK nếu nó hiệu quả hơn về năng lượng (hệ số nhân vectơ được giữ khá bận rộn và có lẽ nó khá ngốn điện, cùng với AVX2 khác ALU vector 256b). OTOH, tôi không biết việc đọc từ đĩa sẽ mất bao nhiêu thời gian từ thứ gì đó đã tối đa hóa tất cả các lõi xử lý đầu vào này. Tôi đoán rằng một chuyển đổi ngữ cảnh để tạo lại trong khối 128k có thể cạnh tranh với việc chạy mã hệ thống tập tin / pagecache và phân bổ các trang để đọc dữ liệu từ đĩa. Tất nhiên, nếu nó đã nóng trong pagecache, thì về cơ bản nó chỉ là memcpy. OTOH, chúng tôi đã viết về nhanh như memcpy! (phải phân chia băng thông bộ nhớ chính giữa đọc và ghi). (Cũng lưu ý rằng viết vào bộ nhớ đó 'rep movsb
(tối ưu hóa memcpy và memset trong microcode, tránh RFO, do Andy Glew triển khai nó trong P6 (Pentium Pro) )).
Cho đến nay, đây chỉ là một bằng chứng về khái niệm và việc xử lý dòng mới chỉ gần đúng. Đó là sai xung quanh các đầu của bộ đệm power-of-2. Với thời gian phát triển hơn. Tôi tự tin rằng tôi có thể tìm ra một cách hiệu quả hơn để chèn các dòng mới cũng chính xác, với chi phí ít nhất là thấp như thế này (so với chỉ xuất ra các khoảng trắng). Tôi nghĩ rằng đây là một cái gì đó giống như 10 đến 20%. Tôi chỉ muốn biết chúng ta có thể chạy nó nhanh như thế nào, chứ không thực sự có một phiên bản bóng bẩy của nó, vì vậy tôi sẽ để phần đó làm bài tập cho người đọc, với các bình luận mô tả một số ý tưởng.
Trên Haswell i5 ở tốc độ tối đa 2,5 GHz, với RAM DDR3-1600 MHz, được định thời tạo ra 100GiB nhưng thu nhỏ lại. (Timed trên cygwin64 trên Win10 với gcc5.4 -O3 -march=native
, bỏ qua -funroll-loops
kể từ khi tôi đã có đủ thời gian khó khăn nhận được thời gian chạy đàng hoàng trên laptop mượn này. Nếu vừa khởi động Linux trên USB).
ghi vào / dev / null trừ khi có quy định khác.
- James Hollis's: (chưa được thử nghiệm)
- Phiên bản fwrite danh nghĩa: ~ 2.21s
- this (SSE2): ~ 0.142s (thời gian không tính toán = real = 14.232s, user = 13.999s, sys = 0.187s).
- này (AVX-128): ~ 0.140s
- this (AVX2): ~ 0,073s (unscaled: real = 0m7.291s, user = 0m7.125s, sys = 0m0.155s).
- cygwin này (AVX2)
wc -c
, với kích thước bộ đệm 128kiB: 0,32s với CPU ở tốc độ 2,38GHz (turbo lõi kép tối đa). (thời gian không tính toán: real = 32.466s user = 11.468s sys = 41.092s, bao gồm cả điều này và wc
). Tuy nhiên, chỉ có một nửa dữ liệu thực sự được sao chép, bởi vì chương trình ngớ ngẩn của tôi cho rằng ghi đó có bộ đệm đầy đủ, mặc dù đó không phải là trường hợp và cygwin write () chỉ thực hiện 64k mỗi cuộc gọi vào một đường ống.
Vì vậy, với SSE2, tốc độ này nhanh hơn khoảng 15 lần so với mã vô hướng của @Nominal Animal. Với AVX2, nó nhanh hơn khoảng 30 lần. Tôi đã không thử một phiên bản mã Nominal chỉ sử dụng write()
thay thế fwrite()
, nhưng có lẽ đối với các bộ đệm lớn, stdio hầu như không sử dụng được. Nếu nó đang sao chép dữ liệu, điều đó sẽ gây ra rất nhiều chậm lại.
Lần để tạo 1GB dữ liệu trên Core2Duo E6600 (Merom 2.4GHz, 32kiB private L1, 4MiB chia sẻ bộ nhớ L2), DDR2-533MHz trong Linux 4.2 bit 64 bit (Ubuntu 15.10). Vẫn sử dụng kích thước bộ đệm 128kiB cho write (), chưa khám phá kích thước đó.
ghi vào / dev / null trừ khi có quy định khác.
- (SSE2) điều này với việc xử lý dòng mới và 4 vectơ chữ số từ mỗi vectơ ngẫu nhiên: 0.183s (tính thời gian thực hiện 100GiB trong 18.3 giây, nhưng kết quả tương tự cho các lần chạy 1GiB). 1,85 hướng dẫn mỗi chu kỳ.
- (SSE2) này, đường ống tới
wc -c
: 0,593s (unscaled: real = 59.266s user = 20.148s sys = 1m6.548s, bao gồm cả thời gian CPU của wc). Cùng một số lần gọi hệ thống write () như với cygwin, nhưng thực tế là tất cả các dữ liệu vì Linux xử lý tất cả 128k của một write () cho một đường ống.
fwrite()
Phiên bản của NominalAnimal (gcc5.2 -O3 -march=native
), chạy với ./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3.19s +/- 0.1%, với hướng dẫn 1.40 mỗi chu kỳ. -funroll-loops có thể là một sự khác biệt nhỏ. clang-3.8 -O3 -march=native
: 3,42 giây +/- 0,1%
- Danh nghĩa-
fwrite
đường ống đến wc -c
: real = 3.980s user = 3.176s sys = 2.080s
- Phiên bản trực tuyến của James Hollis (
clang++-3.8 -O3 -march=native
): 22.885 giây +/- 0,07%, với 0,84 hướng dẫn mỗi chu kỳ. (g ++ 5.2 chậm hơn một chút: 22,98s). Chỉ viết một dòng tại một thời điểm có lẽ bị tổn thương đáng kể.
- Stéphane Chazelas's
tr < /dev/urandom | ...
: real = 41.430s user = 26.832s sys = 40.120s. tr
hầu hết thời gian đã nhận được tất cả lõi CPU, dành gần như toàn bộ thời gian cho trình điều khiển hạt nhân tạo ra các byte ngẫu nhiên và sao chép chúng vào một đường ống. Lõi khác trên máy lõi kép này đang chạy phần còn lại của đường ống.
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: tức là chỉ đọc nhiều ngẫu nhiên mà không có đường ống: real = 35.018s user = 0.036s sys = 34.940s.
- Chương trình perl của Lưu Vĩnh Phúc (perl v5.20.2 từ Ubuntu15.10)
LANG=en_CA.UTF-8
:: real = 4m32.634s user = 4m3.288s sys = 0m29.364.
LC_ALL=C LANG=C
: real = 4m18.637s người dùng = 3m50.324s sys = 0m29.356s. Vẫn rất chậm.
- (SSE2) này không có xử lý xuống dòng , và 3 hoặc 4 vectơ của các chữ số từ mỗi vector của byte ngẫu nhiên (gần như chính xác với tốc độ tương tự: các
dig3 = v%10
bước là về hòa vốn trên HW này): 0.166s (1,82 lệnh trong một chu kỳ) . Về cơ bản, đây là giới hạn thấp hơn cho những gì chúng ta có thể tiến gần đến với việc xử lý dòng mới hoàn toàn hiệu quả.
- (SSE2) Phiên bản cũ của phiên bản này không xử lý dòng mới, nhưng chỉ nhận được một chữ số cho mỗi phần tử uint16_t bằng cách sử dụng
v%10
, 0,222 giây +/- 0,4%, 2,12 hướng dẫn mỗi chu kỳ. (Được biên dịch với gcc5.2 , -march=native -O3 -funroll-loops
. Các vòng lặp không kiểm soát sẽ xảy ra để trợ giúp cho mã này trên phần cứng này. Đừng sử dụng nó một cách mù quáng, đặc biệt là cho các chương trình lớn).
- (SSE2) Phiên bản cũ này, ghi vào một tệp (trên RAID10f2 gồm 3 ổ cứng từ tính nhanh, không được tối ưu hóa cho việc ghi): ~ 4 giây. Có thể đi nhanh hơn bằng cách điều chỉnh cài đặt bộ đệm I / O kernel để cho phép nhiều dữ liệu bẩn hơn trước các khối write (). Thời gian "hệ thống" vẫn là ~ 1,0 giây, cao hơn nhiều so với thời gian "người dùng". Trên hệ thống cũ này có RAM DDR2-533 chậm, hạt nhân sẽ mất nhiều thời gian hơn ~ 4 lần để ghi nhớ dữ liệu vào pagecache và chạy các chức năng XFS so với vòng lặp của tôi để tiếp tục viết lại tại chỗ trong bộ đệm vẫn còn nóng bộ nhớ cache.
Làm thế nào nó được thực hiện
Một PRNG nhanh là rõ ràng cần thiết. xorshift128 + có thể được vector hóa, do đó bạn có hai hoặc bốn bộ tạo 64 bit song song, trong các phần tử của vectơ SIMD. Mỗi bước tạo ra một vectơ đầy đủ của các byte ngẫu nhiên. ( 256b triển khai AVX2 tại đây với nội tại Intel ). Tôi đã chọn nó qua lựa chọn xorshift * của Nominal, bởi vì phép nhân số nguyên vector 64 bit chỉ có thể có trong SSE2 / AVX2 với các kỹ thuật có độ chính xác mở rộng .
Cho một vectơ byte ngẫu nhiên, chúng ta có thể chia từng phần tử 16 bit thành nhiều chữ số thập phân. Chúng tôi tạo ra nhiều vectơ của các phần tử 16 bit là mỗi một không gian ASCII chữ số + ASCII . Chúng tôi lưu trữ trực tiếp vào bộ đệm đầu ra của chúng tôi.
Phiên bản gốc của tôi chỉ được sử dụng x / 6554
để lấy một chữ số ngẫu nhiên từ mọi phần tử uint16_t của một vectơ. Nó luôn nằm trong khoảng từ 0 đến 9, bao gồm. Đó là thiên vị từ 9
, bởi vì (2^16 -1 ) / 6554
chỉ 9,9923. (6554 = ceil ((2 ^ 16-1) / 10), đảm bảo rằng thương số luôn luôn <10.)
x/6554
có thể được tính với một nhân với hằng số "ma thuật" ( đối ứng điểm cố định ) và dịch chuyển đúng của kết quả nửa cao. Đây là trường hợp tốt nhất để chia theo hằng số; một số ước số có nhiều hoạt động hơn và bộ phận đã ký có thêm công việc. x % 10
có thành kiến tương tự và không rẻ để tính toán. (đầu ra asm gcc là tương đương với x - 10*(x/10)
, tức là thêm nhân và trừ trên đầu trang của các bộ phận sử dụng một nghịch đảo mô-đun.) Ngoài ra, các bit thấp nhất của xorshift128 + không phải là chất lượng cao , vì vậy chia để có entropy từ bit cao là tốt hơn ( cho chất lượng cũng như tốc độ) hơn modulo để lấy entropy từ các bit thấp.
Tuy nhiên, chúng ta có thể sử dụng nhiều entropy hơn trong mỗi uint16_t bằng cách xem các chữ số thập phân thấp, như hàm @ Nominal's digit()
. Để có hiệu suất tối đa, tôi quyết định lấy 3 chữ số thập phân thấp và x/6554
, để lưu một PMULLW và PSUBW (và có thể là một số MOVDQA) so với tùy chọn chất lượng cao hơn là lấy 4 chữ số thập phân thấp. x / 6554 bị ảnh hưởng đôi chút bởi 3 chữ số thập phân thấp, do đó, có một số mối tương quan giữa các chữ số từ cùng một phần tử (tách 8 hoặc 16 chữ số trong đầu ra ASCII, tùy thuộc vào độ rộng của vectơ).
Tôi nghĩ gcc đang chia cho 100 và 1000, thay vì chuỗi dài hơn chia liên tiếp cho 10, vì vậy có lẽ nó không rút ngắn đáng kể độ dài của chuỗi phụ thuộc không mang theo vòng lặp tạo ra 4 kết quả từ mỗi đầu ra PRNG. port0 (vectơ nhân và dịch chuyển) là nút cổ chai do các phép nghịch đảo nhân mô-đun và các dịch chuyển trong xorshift +, do đó, chắc chắn rất hữu ích để lưu bội nhân vectơ.
xorshift + nhanh đến mức thậm chí chỉ sử dụng ~ 3,3 bit ngẫu nhiên từ mỗi 16 (tức là hiệu suất 20%) không chậm hơn nhiều so với việc cắt nó thành nhiều chữ số thập phân. Chúng tôi chỉ ước tính phân phối đồng đều, vì câu trả lời này tập trung vào tốc độ miễn là chất lượng không quá tệ.
Bất kỳ loại hành vi có điều kiện nào giữ một số lượng phần tử thay đổi sẽ tốn nhiều công sức hơn. (Nhưng có thể vẫn có thể được thực hiện một cách hiệu quả bằng cách sử dụng các kỹ thuật đóng gói trái SIMD . Tuy nhiên, điều đó kém hiệu quả hơn đối với kích thước phần tử nhỏ; các bảng tra cứu mặt nạ xáo trộn khổng lồ là không thể thực hiện được và không có xáo trộn làn đường AVX2 với nhỏ hơn 32- Các phần tử bit. Phiên bản PSHUFB 128b vẫn có thể tạo mặt nạ khi đang di chuyển với BMI2 PEXT / PDEP, giống như bạn có thể cho AVX2 với các phần tử lớn hơn , nhưng thật khó khăn vì số nguyên 64 bit chỉ chứa 8 byte. trên câu trả lời đó có một số mã có thể hoạt động cho số phần tử cao hơn.)
Nếu độ trễ của RNG là một nút cổ chai, chúng ta có thể đi nhanh hơn nữa bằng cách chạy song song hai vectơ máy phát, xen kẽ cái nào chúng ta sử dụng. Trình biên dịch vẫn có thể dễ dàng giữ mọi thứ trong các thanh ghi trong một vòng lặp không được kiểm soát và điều đó cho phép hai chuỗi phụ thuộc chạy song song.
Trong phiên bản hiện tại, cắt giảm đầu ra của PRNG, chúng tôi thực sự bị tắc nghẽn về thông lượng cổng 0, không phải độ trễ PRNG, do đó không cần điều đó.
Mã: phiên bản AVX2
Phiên bản đầy đủ với nhiều bình luận hơn về trình thám hiểm trình biên dịch Godbolt .
Không gọn gàng lắm, xin lỗi tôi phải đi ngủ và muốn đăng cái này lên.
Để có được phiên bản SSE2, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
, và thay đổi vector_size(32)
đến 16. Ngoài ra thay đổi thặng dư xuống dòng từ 4 * 16-4 * 8. (Như tôi đã nói, mã rất lộn xộn và không được thiết lập tốt để biên dịch hai phiên bản. Ban đầu, tôi không có kế hoạch tạo phiên bản AVX2, nhưng sau đó tôi thực sự muốn thử nghiệm CPU Haswell mà tôi có quyền truy cập.)
#include <immintrin.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
//#include <string.h>
// This would work equally fast 128b or 256b at a time (AVX2):
// https://stackoverflow.com/questions/24001930/avx-sse-version-of-xorshift128
struct rngstate256 {
__m256i state0;
__m256i state1;
};
static inline __m256i xorshift128plus_avx2(struct rngstate256 *sp)
{
__m256i s1 = sp->state0;
const __m256i s0 = sp->state1;
sp->state0 = s0;
s1 = _mm256_xor_si256(s1, _mm256_slli_epi64(s1, 23));
__m256i state1new = _mm256_xor_si256(_mm256_xor_si256(_mm256_xor_si256(s1, s0),
_mm256_srli_epi64(s1, 18)),
_mm256_srli_epi64(s0, 5));
sp->state1 = state1new;
return _mm256_add_epi64(state1new, s0);
}
// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));
__m256i* vec_store_digit_and_space(__m256i vec, __m256i *restrict p)
{
v16u v = (v16u)vec;
v16u ten = (v16u)_mm256_set1_epi16(10);
v16u divisor = (v16u)_mm256_set1_epi16(6554); // ceil((2^16-1) / 10.0)
v16u div6554 = v / divisor; // Basically the entropy from the upper two decimal digits: 0..65.
// Probably some correlation with the modulo-based values, especially dig3, but we do this instead of
// dig4 for more ILP and fewer instructions total.
v16u dig1 = v % ten;
v /= ten;
v16u dig2 = v % ten;
v /= ten;
v16u dig3 = v % ten;
// dig4 would overlap much of the randomness that div6554 gets
const v16u ascii_digitspace = (v16u)_mm256_set1_epi16( (' '<<8) | '0');
v16u *vecbuf = (v16u*)p;
vecbuf[0] = div6554 | ascii_digitspace;
vecbuf[1] = dig1 | ascii_digitspace;
vecbuf[2] = dig2 | ascii_digitspace;
vecbuf[3] = dig3 | ascii_digitspace;
return p + 4; // always a constant number of full vectors
}
void random_decimal_fill_buffer(char *restrict buf, size_t len, struct rngstate256 *restrict rngstate)
{
buf = __builtin_assume_aligned(buf, 32);
// copy to a local so clang can keep state in register, even in the non-inline version
// restrict works for gcc, but apparently clang still thinks that *buf might alias *rngstate
struct rngstate256 rng_local = *rngstate;
__m256i *restrict p = (__m256i*restrict)buf;
__m256i *restrict endbuf = (__m256i*)(buf+len);
static unsigned newline_pos = 0;
do {
__m256i rvec = xorshift128plus_avx2(&rng_local);
p = vec_store_digit_and_space(rvec, p); // stores multiple ASCII vectors from the entropy in rvec
#if 1
// this is buggy at the end or start of a power-of-2 buffer:
// usually there's a too-short line, sometimes a too-long line
const unsigned ncols = 100;
newline_pos += 4*16;
if (newline_pos >= ncols) {
newline_pos -= ncols;
char *cur_pos = (char*)p;
*(cur_pos - newline_pos*2 - 1) = '\n';
}
#endif
// Turning every 100th space into a newline.
// 1) With an overlapping 1B store to a location selected by a counter. A down-counter would be more efficient
// 2) Or by using a different constant for ascii_digitspace to put a newline in one element
// lcm(200, 16) is 400 bytes, so unrolling the loop enough to produce two full lines makes a pattern of full vectors repeat
// lcm(200, 32) is 800 bytes
// a power-of-2 buffer size doesn't hold a whole number of lines :/
// I'm pretty sure this can be solved with low overhead, like maybe 10% at worst.
} while(p <= endbuf-3);
*rngstate = rng_local;
}
#define BUFFER_SIZE (128 * 1024)
const static size_t bufsz = BUFFER_SIZE;
__attribute__((aligned(64))) static char static_buf[BUFFER_SIZE];
int main(int argc, char *argv[])
{
// TODO: choose a seed properly. (Doesn't affect the speed)
struct rngstate256 xorshift_state = {
_mm256_set_epi64x(123, 456, 0x123, 0x456),
_mm256_set_epi64x(789, 101112, 0x789, 0x101112)
};
for (int i=0; i < 1024ULL*1024*1024 / bufsz * 100; i++) {
random_decimal_fill_buffer(static_buf, bufsz, &xorshift_state);
size_t written = write(1, static_buf, bufsz);
(void)written;
//fprintf(stderr, "wrote %#lx of %#lx\n", written, bufsz);
}
}
Biên dịch với gcc, clang hoặc ICC (hoặc hy vọng bất kỳ trình biên dịch nào khác hiểu phương ngữ GNU C của C99 và nội tại của Intel). Các phần mở rộng vectơ GNU C rất thuận tiện để có được trình biên dịch tạo ra các số ma thuật cho phép chia / modulo bằng cách sử dụng các phép nghịch đảo mô đun, và thỉnh thoảng __attribute__
s rất hữu ích.
Điều này có thể được viết một cách hợp lý, nhưng nó sẽ mất nhiều mã hơn.
Ghi chú hiệu suất:
Cửa hàng chồng chéo để chèn dòng mới có chi phí đáng kể để quyết định vị trí đặt nó (dự đoán sai chi nhánh và tắc nghẽn giao diện trên Core2), nhưng bản thân cửa hàng không ảnh hưởng đến hiệu suất. Chỉ nhận xét rằng hướng dẫn lưu trữ trong asm của trình biên dịch (để tất cả các nhánh giống nhau) khiến hiệu suất trên Core2 hoàn toàn không thay đổi, với các lần chạy lặp lại cho cùng thời gian tới +/- dưới 1%. Vì vậy, tôi kết luận rằng bộ đệm / bộ đệm lưu trữ xử lý nó tốt.
Tuy nhiên, sử dụng một số loại cửa sổ xoay ascii_digitspace
với một yếu tố có dòng mới có thể còn nhanh hơn nữa, nếu chúng ta không kiểm soát đủ để bất kỳ bộ đếm / phân nhánh nào biến mất.
Ghi vào / dev / null về cơ bản là không có op, vì vậy bộ đệm có thể vẫn nóng trong bộ đệm L2 (256kiB trên mỗi lõi trên Haswell). Việc tăng tốc hoàn hảo từ vectơ 128b lên vectơ 256b được mong đợi: không có hướng dẫn thêm và mọi thứ (bao gồm các cửa hàng) xảy ra với chiều rộng gấp đôi. Tuy nhiên, nhánh chèn dòng mới được thực hiện gấp đôi. Tôi không may đã không có thời gian trên thiết lập cygwin Haswell của tôi với phần đó được chỉnh sửa #ifdef
.
2,5 GHz * 32B / 13,7GB / s = 5,84 chu kỳ trên mỗi cửa hàng AVX2 trên Haswell. Điều đó khá tốt, nhưng có thể nhanh hơn. Có lẽ có một số chi phí trong hệ thống cygwin gọi hơn tôi nghĩ. Tôi đã không thử nhận xét những điều đó trong đầu ra asm của trình biên dịch (điều này sẽ đảm bảo rằng không có gì được tối ưu hóa.)
Bộ nhớ cache L1 có thể duy trì một cửa hàng 32B mỗi đồng hồ và L2 không phải là băng thông thấp hơn nhiều (tuy nhiên độ trễ cao hơn).
Khi tôi xem IACA một vài phiên bản trước đây (không phân nhánh cho dòng mới, nhưng chỉ nhận được một vectơ ASCII trên mỗi vectơ RNG), nó đã dự đoán một cái gì đó giống như một cửa hàng véc tơ 32B trên 4 hoặc 5 đồng hồ.
Tôi đã hy vọng sẽ nhận được nhiều sự tăng tốc hơn từ việc trích xuất thêm dữ liệu từ mỗi kết quả RNG, dựa trên việc xem xét bản thân, xem xét các hướng dẫn của Agner Fog và các tài nguyên tối ưu hóa khác mà tôi đã thêm các liên kết trong wiki thẻ SO x86 .)
Có khả năng nó sẽ nhanh hơn đáng kể trên Skylake , trong đó nhân số nguyên và dịch chuyển có thể chạy trên hai lần số cổng (p0 / p1) gấp đôi so với Haswell (chỉ p0). xorshift và trích xuất chữ số đều sử dụng rất nhiều ca và nhân. ( Cập nhật: Skylake chạy nó ở 3.02 IPC, cung cấp cho chúng tôi 3,77 chu kỳ trên mỗi cửa hàng AVX2 32 byte , thời gian là 0,030 giây trên mỗi lần lặp 1GB, ghi /dev/null
vào Linux 4.15 trên i7-6700k ở tốc độ 3,9 GHz.
Nó không yêu cầu chế độ 64 bit để hoạt động tốt . Phiên bản SSE2 chỉ nhanh khi được biên dịch -m32
, vì nó không cần nhiều thanh ghi vectơ và tất cả toán học 64 bit được thực hiện trong các vectơ, không phải các thanh ghi mục đích chung.
Nó thực sự nhanh hơn một chút trong chế độ 32 bit trên Core2, bởi vì phản ứng tổng hợp macro / nhánh chỉ hoạt động ở chế độ 32 bit, do đó, có ít lỗi hơn cho lõi không theo thứ tự (18.3s (1.85 Hướng dẫn trên mỗi đồng hồ) so với 16,9 giây (2.0 IPC)). Kích thước mã nhỏ hơn do không có tiền tố REX cũng giúp bộ giải mã của Core2.
Ngoài ra, một số di chuyển vector reg-reg được thay thế bằng tải, vì không phải tất cả các hằng số sửa trong regs vector nữa. Vì thông lượng tải từ bộ đệm L1 không phải là nút cổ chai, điều này thực sự có ích. (ví dụ: nhân với một vectơ không đổi là set1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
biến thành movdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) Vì reg-reg MOVDQA yêu cầu cổng ALU, nó cạnh tranh với công việc thực tế đang được thực hiện, nhưng tải MOVDQA chỉ cạnh tranh về băng thông giải mã mặt trước. (Có một địa chỉ 4 byte bên trong nhiều hướng dẫn sẽ loại bỏ rất nhiều lợi ích từ việc lưu tiền tố REX.
Tôi sẽ không ngạc nhiên nếu việc lưu các ALU MOVDQA là nơi thu được lợi nhuận thực sự, vì tiền tuyến phải theo kịp với mức trung bình 2.0 IPC khá tốt.
Tất cả những khác biệt này biến mất trên Haswell, nơi toàn bộ mọi thứ sẽ chạy từ bộ đệm được giải mã, nếu không phải là bộ đệm loopback. ALU + hợp nhất vĩ mô nhánh hoạt động ở cả hai chế độ kể từ Nehalem.