Jeśli nie potrzebujesz losowości o bardzo wysokiej jakości, a wystarczająca jest prawie równomierna dystrybucja, możesz iść naprawdę szybko, szczególnie na nowoczesnym procesorze z wydajnymi wektorami całkowitymi SIMD, takimi jak x86 z SSE2 lub AVX2.
To jest jak odpowiedź @ NominalAnimal, ponieważ oboje mieliśmy ten sam pomysł, ale ręcznie wektoryzowaliśmy dla x86. (A przy liczbach losowych gorszej jakości, ale prawdopodobnie wystarczających do wielu przypadków użycia). Działa to około 15 lub 30 razy szybciej niż kod @ Nominal, przy ~ 13 GB / s wyjścia ASCII na Intel Haswell 2,5 GHz Procesor z AVX2. To wciąż mniej niż teoretyczna maksymalna przepustowość pamięci głównej (dwukanałowa pamięć DDR3-1600 wynosi około 25,6 GB / s), ale tak naprawdę zapisywałem czas do / dev / null, więc w rzeczywistości po prostu przepisałem bufor, który pozostaje gorący w pamięci podręcznej. Skylake powinien uruchomić ten sam kod znacznie szybciej niż Haswell (patrz na dole tej odpowiedzi).
Zakładając, że faktycznie masz wąskie gardło we / wy na dysku lub gdzieś to potokujesz, szybka implementacja oznacza, że twój procesor nie musi nawet taktować się wyżej niż bezczynnie. Zużywa znacznie mniej energii całkowitej do uzyskania wyniku. (Żywotność baterii / ciepło / globalne ocieplenie.)
Jest to tak szybkie, że prawdopodobnie nie chcesz zapisywać go na dysku. Po prostu ponownie wygeneruj w razie potrzeby (z tego samego materiału siewnego, jeśli chcesz ponownie te same dane). Nawet jeśli chcesz go przesłać do wielowątkowego procesu, który może korzystać ze wszystkich procesorów, uruchomienie tego w celu przesłania danych do niego spowoduje pozostawienie go w pamięci podręcznej L3 (i pamięci podręcznej L2 na rdzeniu, który go napisał), i użyj tak bardzo mały czas procesora. (Należy jednak pamiętać, że /dev/null
przesyłanie strumieniowe dodaje dużo narzutu w porównaniu do pisania . W Skylake i7-6700k, przesyłanie do wc -c
lub innego programu, który tylko czyta + odrzuca dane wejściowe, jest około 8 razy wolniejsze niż zapisywanie do/dev/null
i wykorzystuje tylko 70% Procesor, ale to wciąż 4,0 GB / s na procesorze 3,9 GHz.
Ponowne wygenerowanie jest szybsze niż ponowne odczytanie go nawet z szybkiego dysku SSD podłączonego przez PCIe, ale IDK, jeśli jest bardziej energooszczędny (multiplikator wektor-liczba jest nadal zajęty i prawdopodobnie jest dość energochłonny, podobnie jak inne AVX2 256 ALU wektorów). OTOH, nie wiem, ile czasu procesora czytającego z dysku zabrałoby coś, co maksymalizowało wszystkie rdzenie przetwarzające to wejście. Domyślam się, że przełącznik kontekstowy do ponownego generowania w porcjach 128k może być konkurencyjny w stosunku do uruchamiania kodu systemu plików / pagecache i przydzielania stron do odczytu danych z dysku. Oczywiście, jeśli jest już gorąco w pamięci podręcznej, to po prostu jest zapadający w pamięć. OTOH, piszemy już o tak szybkim jak memcpy! (która musi rozdzielić przepustowość pamięci głównej na odczyt i zapis). (Należy również pamiętać, że zapisywanie w pamięci, że „rep movsb
(zoptymalizowany memcpy i memset w mikrokodzie, co pozwala uniknąć RFO, ponieważ Andy Glew zaimplementował go w P6 (Pentium Pro )).
Jak dotąd jest to tylko dowód koncepcji, a obsługa nowej linii jest tylko w przybliżeniu poprawna. Jest źle na końcach bufora power-of-2. Więcej czasu na rozwój. Jestem pewien, że mógłbym znaleźć bardziej skuteczny sposób wstawiania znaków nowej linii, który jest również dokładnie poprawny, z co najmniej tak niskim narzutem (w porównaniu do wypisywania tylko spacji). Myślę, że jest to około 10 do 20%. Interesuje mnie tylko to, jak szybko możemy uruchomić ten bieg, a nie faktyczna jego dopracowana wersja, więc zostawię tę część jako ćwiczenie dla czytelnika, z komentarzami opisującymi niektóre pomysły.
Na Haswell i5 z maksymalnym turbodoładowaniem 2,5 GHz, z pamięcią RAM DDR3-1600 MHz , czasowo generuje 100GiB, ale został zmniejszony. (Czasowo na cygwin64 na Win10 z gcc5.4 -O3 -march=native
, pominięty, -funroll-loops
ponieważ miałem dość czasu na uzyskanie przyzwoitego czasu na tym pożyczonym laptopie. Powinienem właśnie uruchomić Linuksa na USB).
pisanie do / dev / null, chyba że określono inaczej.
- James Hollis: (nie testowano)
- Nominalna wersja fwrite: ~ 2.21s
- to (SSE2): ~ 0,142 s (nieskalowane czasy = rzeczywiste = 14,222 s, użytkownik = 13,999 s, sys = 0,187 s).
- to (AVX-128): ~ 0.140s
- to (AVX2): ~ 0,073s (nieskalowane: real = 0m7,291s, użytkownik = 0m7,125s, sys = 0m0,155s).
- to (AVX2) przesyłanie cygwin do
wc -c
bufora o rozmiarze 128 kB: 0,32 s z procesorem 2,38 GHz (maks. dwurdzeniowe turbo). (nieskalowane czasy: real = 32.466s użytkownik = 11.468s sys = 41.092s, w tym zarówno to, jak i wc
). Jednak tylko połowa danych została skopiowana, ponieważ mój głupi program zakłada, że zapis zajmuje pełny bufor, nawet jeśli tak nie jest, a cygwin write () robi tylko 64k na wywołanie do potoku.
W przypadku SSE2 jest to około 15 razy szybsze niż kod skalarny @Nominal Animal. Dzięki AVX2 jest około 30 razy szybszy. Nie wypróbowałem wersji kodu Nominal, która po prostu używa write()
zamiast tego fwrite()
, ale przypuszczalnie dla dużych buforów stdio zwykle nie przeszkadza . Jeśli kopiuje dane, spowodowałoby to spowolnienie.
Czasy do wyprodukowania 1 GB danych na Core2Duo E6600 (Merom 2.4GHz, 32kB prywatny L1, 4MiB współdzielone pamięci podręczne L2), DDR2-533MHz w 64-bitowym Linuksie 4.2 (Ubuntu 15.10). Nadal używając rozmiaru bufora 128kiB do write (), nie zbadałem tego wymiaru.
pisanie do / dev / null, chyba że określono inaczej.
- (SSE2) to z obsługą nowej linii i 4 wektorami cyfr z każdego wektora losowych bajtów: 0,183 s (czas wykonania 100 GiB w 18,3 s, ale podobne wyniki dla przebiegów 1 GiB). 1,85 instrukcji na cykl.
- (SSE2) to, przesyłanie do
wc -c
: 0,593 s (nieskalowane: rzeczywiste = 59,266s użytkownik = 20,148s sys = 1m6,548s, w tym czas procesora wc). Taka sama liczba wywołań systemowych write () jak w przypadku cygwin, ale faktycznie przesyłanie wszystkich danych, ponieważ Linux obsługuje wszystkie 128k zapisu () do potoku.
- NominalAnimal jest
fwrite()
wersja (gcc5.2 -O3 -march=native
), należy uruchomić z ./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3.19s +/- 0,1%, przy 1,40 instrukcji na cykl. -funrollowe pętle zrobiły może małą różnicę. clang-3.8 -O3 -march=native
: 3,42s +/- 0,1%
- Nominalny potok
fwrite
do wc -c
: rzeczywisty = 3,980s użytkownik = 3,176s sys = 2,080s
- Wersja liniowa Jamesa Hollisa (
clang++-3.8 -O3 -march=native
): 22,885s +/- 0,07%, z 0,84 instrukcjami na cykl. (g ++ 5.2 był nieco wolniejszy: 22,98s). Pisanie tylko jednej linii na raz prawdopodobnie bolało znacznie.
- Stéphane Chazelas
tr < /dev/urandom | ...
: real = 41.430s użytkownik = 26,832s sys = 40.120s. tr
przez większość czasu zajmował się samym rdzeniem procesora, spędzając prawie cały czas w sterowniku jądra, generując losowe bajty i kopiując je do potoku. Drugi rdzeń na tym dwurdzeniowym komputerze działał przez resztę rurociągu.
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: tzn. po prostu odczytuje tyle losowości bez orurowania: real = 35.018s użytkownik = 0.036s sys = 34.940s.
- Program perla Lưu Vĩnh Phúc (perl v5.20.2 z Ubuntu15.10)
LANG=en_CA.UTF-8
:: real = 4m32.634s użytkownik = 4m3.288s sys = 0m29.364.
LC_ALL=C LANG=C
: real = 4m18.637s użytkownik = 3m50.324s sys = 0m29.356s Wciąż bardzo powoli.
- (SSE2) to bez obsługi nowego wiersza i albo 3 lub 4 wektory cyfr z każdego wektora losowych bajtów (prawie dokładnie taka sama prędkość:
dig3 = v%10
krok dotyczy progu rentowności na tym CWU): 0,166 s (1,82 instrukcji na cykl) . Jest to w zasadzie dolna granica tego, do czego możemy się zbliżyć dzięki idealnie wydajnej obsłudze nowej linii.
- (SSE2) Stara wersja tego bez obsługi nowego wiersza, ale tylko jedna cyfra na element uint16_t przy użyciu
v%10
, 0,222 sekundy +/- 0,4%, 2,12 instrukcji na cykl. (Skompilowane z gcc5.2 -march=native -O3 -funroll-loops
. Pętle Unroll pomagają w tym kodzie na tym sprzęcie. Nie używaj go na ślepo, szczególnie w przypadku dużych programów).
- (SSE2) Stara wersja tego, zapis do pliku (na RAID10f2 z 3 szybkich magnetycznych dysków twardych, niezbyt zoptymalizowanych do zapisu): ~ 4 sekundy. Mógłby pójść szybciej przez ulepszenie ustawień bufora we / wy jądra, aby pozwolić na znacznie więcej brudnych danych przed blokami write (). Czas „systemowy” wciąż wynosi ~ 1,0 sekundy, czyli znacznie więcej niż czas „użytkownika”. W tym starym systemie z powolną pamięcią RAM DDR2-533 jądro zapamiętuje dane do pamięci podręcznej i uruchamia funkcje XFS ~ 4x dłużej niż w mojej pętli, aby zapisywać je ponownie w buforze, który pozostaje gorący Pamięć podręczna.
Jak to jest zrobione
Szybki PRNG jest oczywiście niezbędny. xorshift128 + można wektoryzować, dzięki czemu masz dwa lub cztery 64-bitowe generatory równolegle w elementach wektora SIMD. Każdy krok tworzy pełny wektor losowych bajtów. ( Tutaj implementacja AVX2 256b z wbudowanymi procesorami Intela ). Wybrałem to w porównaniu z wyborem xorshift * Nominal, ponieważ 64-bitowe zwielokrotnienie liczb całkowitych wektorów jest możliwe tylko w SSE2 / AVX2 z technikami o zwiększonej precyzji .
Biorąc pod uwagę wektor losowych bajtów, możemy pokroić każdy 16-bitowy element na wiele cyfr dziesiętnych. Produkujemy wiele wektorów 16-bitowych elementów, z których każdy jest jedną cyfrą ASCII + spacją ASCII . Przechowujemy to bezpośrednio w naszym buforze wyjściowym.
Moja oryginalna wersja właśnie x / 6554
pobierała jedną losową cyfrę z każdego elementu wektora uint16_t. Zawsze wynosi od 0 do 9 włącznie. Jest tendencyjny 9
, ponieważ (2^16 -1 ) / 6554
wynosi tylko 9.99923. (6554 = Ceil ((2 ^ 16-1) / 10), co zapewnia, że iloraz jest zawsze <10.)
x/6554
można obliczyć z jednym pomnożeniem przez stałą „magiczną” ( odwrotność stałego punktu ) i prawidłowe przesunięcie wyniku wysokiej połowy. To najlepszy przypadek dzielenia przez stałą; niektóre dzielniki wymagają więcej operacji, a podpisany podział wymaga dodatkowej pracy. x % 10
ma podobny błąd i nie jest tak tani w obliczeniach. (wyjście asm gcc jest równoważne x - 10*(x/10)
, tj. dodatkowe zwielokrotnienie i odjęcie na górze podziału za pomocą modularnego odwrotności multiplikatywnej). Również najniższy bit xorshift128 + nie jest tak wysokiej jakości , więc dzielenie się, aby wziąć entropię z wysokich bitów, jest lepsze ( dla jakości, jak również prędkości) niż modulo, aby pobrać entropię z małych bitów.
Możemy jednak użyć więcej entropii w każdym uint16_t, patrząc na małe cyfry dziesiętne, takie jak digit()
funkcja @ Nominal . Aby uzyskać maksymalną wydajność, postanowiłem wziąć 3 małe cyfry dziesiętne i x/6554
, aby zapisać jeden PMULLW i PSUBW (i prawdopodobnie niektóre MOVDQA) w porównaniu z opcją wyższej jakości, biorąc 4 niskie cyfry dziesiętne. Niskie 3 cyfry dziesiętne mają nieznaczny wpływ na x / 6554, więc istnieje pewna korelacja między cyframi z tego samego elementu (separacja 8 lub 16 cyfr na wyjściu ASCII, w zależności od szerokości wektora).
Myślę, że gcc dzieli przez 100 i 1000, zamiast dłuższego łańcucha, który sukcesywnie dzieli się przez 10, więc prawdopodobnie nie skraca znacząco długości łańcucha zależności nie przenoszonej przez pętlę, który daje 4 wyniki z każdego wyjścia PRNG. port0 (mnożenie i przesuwanie wektora) jest wąskim gardłem ze względu na modułowe odwrotne multiplikacje i przesunięcia w xorshift +, więc zdecydowanie warto zapisać wielokrotność wektora.
xorshift + jest tak szybki, że nawet użycie tylko ~ 3,3 bitów losowości na każde 16 (tj. 20% wydajności) nie jest dużo wolniejsze niż dzielenie go na wiele cyfr dziesiętnych. Przybliżamy jedynie rozkład równomierny, ponieważ odpowiedź ta koncentruje się na szybkości, o ile jakość nie jest tak zła.
Wszelkie zachowania warunkowe utrzymujące zmienną liczbę elementów wymagałyby znacznie więcej pracy. (Ale może nadal być to nieco wydajne przy użyciu technik upakowywania po lewej stronie SIMD . Jednak staje się to mniej wydajne dla małych rozmiarów elementów; gigantyczne tabele wyszukiwania z maską losową nie są wykonalne i nie ma tasowania z przecinaniem linii AVX2 z mniejszym niż 32- elementy bitowe. Wersja PSHUFB 128b może nadal być w stanie wygenerować maskę w locie za pomocą BMI2 PEXT / PDEP, podobnie jak w przypadku AVX2 z większymi elementami , ale jest to trudne, ponieważ 64-bitowa liczba całkowita zawiera tylko 8 bajtów. Godbolt link w tej odpowiedzi znajduje się kod, który może działać w przypadku większej liczby elementów).
Jeśli opóźnienie RNG jest wąskim gardłem, moglibyśmy iść jeszcze szybciej, uruchamiając równolegle dwa wektory generatorów, naprzemiennie z których korzystamy. Kompilator nadal może łatwo przechowywać wszystko w rejestrach w rozwiniętej pętli, co pozwala na równoległe działanie dwóch łańcuchów zależności.
W obecnej wersji, dzieląc dane wyjściowe PRNG, faktycznie wąskie gardło w przepustowości portu 0, a nie w opóźnieniu PRNG, więc nie ma takiej potrzeby.
Kod: wersja AVX2
Pełna wersja z większą ilością komentarzy na temat eksploratora kompilatora Godbolt .
Niezbyt schludny, przepraszam, muszę iść spać i chcę to opublikować.
Aby uzyskać wersję SSE2, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
, i zmienić vector_size(32)
do 16. Również zmienić przyrost nowej linii od 16 do 4 * 4 * 8. (Tak jak powiedziałem, kod jest nieporządny i nie jest dobrze skonfigurowany do kompilacji dwóch wersji. Nie planowałem początkowo tworzenia wersji AVX2, ale potem naprawdę chciałem przetestować procesor Haswell, do którego miałem dostę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);
}
}
Kompiluj za pomocą gcc, clang lub ICC (lub mam nadzieję, że jakikolwiek inny kompilator, który rozumie dialekt GNU C C99 i wewnętrzne cechy Intela). Rozszerzenia wektorów GNU C są bardzo wygodne, aby kompilator generował magiczne liczby dla dzielenia / modulo przy użyciu modularnych odwrotności multiplikatywnych, a okazjonalne __attribute__
s są przydatne.
Można to zapisać przenośnie, ale zajmie to więcej kodu.
Uwagi dotyczące wydajności:
Nakładający się sklep do wstawiania nowych linii ma znaczny narzut, aby zdecydować, gdzie go umieścić (nieprzewidywalne rozgałęzienia i wąskie gardła nakładki na Core2), ale sam sklep nie ma wpływu na wydajność. Komentowanie tylko tej instrukcji sklepu w asmie kompilatora (pozostawiając wszystkie rozgałęzienia bez zmian) pozostawiło wydajność Core2 całkowicie niezmienioną, a powtarzane przebiegi dały ten sam czas do +/- mniej niż 1%. Doszedłem więc do wniosku, że bufor / pamięć podręczna radzą sobie z tym dobrze.
Mimo to użycie pewnego rodzaju okna obrotowego ascii_digitspace
z jednym elementem o nowej linii może być jeszcze szybsze, jeśli rozwiniemy się na tyle, że znikną jakiekolwiek liczniki / rozgałęzienia.
Zapisywanie do / dev / null jest w zasadzie brakiem operacji, więc bufor prawdopodobnie pozostaje gorący w pamięci podręcznej L2 (256 kB na rdzeń w Haswell). Oczekuje się idealnego przyspieszenia z wektorów 128b do wektorów 256b: nie ma dodatkowych instrukcji, a wszystko (łącznie ze sklepami) dzieje się z podwójną szerokością. Jednak gałąź wstawiania nowej linii jest pobierana dwa razy częściej. Niestety nie miałem czasu na konfigurację cygwina Haswella z tą częścią #ifdef
.
2,5 GHz * 32B / 13,7 GB / s = 5,84 cykli na sklep AVX2 na Haswell. To całkiem nieźle, ale może być szybsze. Może w wywołaniach systemowych cygwin jest trochę narzutów, niż myślałem. Nie próbowałem komentować tych w danych wyjściowych asm kompilatora (co zapewni, że nic nie zostanie zoptymalizowane).
Pamięć podręczna L1 może obsługiwać jeden magazyn 32B na zegar, a L2 nie jest znacznie mniejszą przepustowością (chociaż większe opóźnienie).
Kiedy spojrzałem na IACA kilka wersji temu (bez rozgałęzienia dla nowych linii, ale otrzymując tylko jeden wektor ASCII na wektor RNG), przewidywałem coś w rodzaju jednego sklepu wektorowego 32B na 4 lub 5 zegarów.
Miałem nadzieję przyspieszyć wydobywanie większej ilości danych z każdego wyniku RNG, na podstawie samego spojrzenia na asm, biorąc pod uwagę przewodniki Agner Fog i inne zasoby optymalizacyjne, do których dodałem linki w wiki tagu SO x86 ).
Prawdopodobnie byłoby to znacznie szybsze na Skylake , gdzie mnożenie liczb całkowitych wektora i przesunięcie może działać na dwukrotnie większej liczbie portów (p0 / p1) w porównaniu do Haswella (tylko p0). Xorshift i ekstrakcja cyfr używają wielu przesunięć i mnożeń. ( Aktualizacja: Skylake działa na 3.02 IPC, co daje nam 3,77 cykli na 32-bajtowy sklep AVX2 , z czasem 0,030s na 1 GB iteracji, pisząc do /dev/null
Linux 4.15 na i7-6700k przy 3,9 GHz.
Do poprawnego działania nie wymaga trybu 64-bitowego . Wersja SSE2 jest równie szybka po kompilacji -m32
, ponieważ nie potrzebuje bardzo wielu rejestrów wektorowych, a cała 64-bitowa matematyka jest wykonywana w wektorach, a nie w rejestrach ogólnego przeznaczenia.
W rzeczywistości jest nieco szybszy w trybie 32-bitowym na Core2, ponieważ makro-fuzja porównania / rozgałęzienia działa tylko w trybie 32-bitowym, więc jest mniej ulepszeń rdzenia poza kolejnością (18,3 s (1,85 instrukcji na zegar) vs 16,9 s (2,0 IPC)). Mniejszy rozmiar kodu, ponieważ nie ma przedrostków REX, pomaga również dekoderom Core2.
Ponadto niektóre ruchy wektorowe reg-reg są zastępowane obciążeniami, ponieważ nie wszystkie stałe są już ustalane w regach wektorowych. Ponieważ przepustowość ładowania z pamięci podręcznej L1 nie jest wąskim gardłem, to w rzeczywistości pomaga. (np. pomnożenie przez stały wektor set1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
zamienia się w movdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) Ponieważ reg-reg MOVDQA wymaga portu ALU, konkuruje on z wykonaną pracą, ale obciążenie MOVDQA konkuruje tylko o szerokość pasma dekodowania interfejsu. (Posiadanie 4-bajtowego adresu w wielu instrukcjach anuluje wiele korzyści z zapisywania prefiksów REX.
Nie zdziwiłbym się, gdyby uratowanie ALU MOVDQA było źródłem prawdziwych korzyści, ponieważ frontend powinien nadążać za średnią 2.0 IPC.
Wszystkie te różnice znikają na Haswell, gdzie cała sprawa powinna przebiegać z odkodowanej pamięci podręcznej, jeśli nie z bufora sprzężenia zwrotnego. Makro-synteza gałęzi ALU + działa w obu trybach od Nehalem.