Testuję sobie różne generatory. Po tym jak kilka osób pomogło mi ogarnąć quick-bench.com mam implementacje i testy:
https://quick-bench.com/q/E8kPDt4GGHv1_MoJwz7ZTsLtHGM
Niedługo zamierzam opublikować swój własny PRNG i profesor, z którym pracuję powiedział mi, że takie pomiary to jest "academic junk" (także to: https://prng.di.unimi.it). Cykle na bajt są może i dobre do publikacji naukowej, jak twierdzi, ale my chcemy zaproponować rozwiązania dla biznesu high-tech. Mówi, że wielu inżynierów porównuje algorytmy pod względem MIPS (miliony instrukcji na sekundę), choć to też nie jest idealna miara (większość chciałaby zobaczyć wyniki na konkretnym sprzęcie i środowisku, na którym sami pracują).
Mówi, że, jeśli już coś mamy porównać na ogólnym poziomie, to oszacować liczbę cykli, licząc ręcznie operacje. Zrobiłem takie wyliczenia:
class xoroshiro256plus {
uint64_t s[4] = { 5, 11, 13, 99 };
static uint64_t rotl(const uint64_t x, int k)
{
return (x << k) | (x >> (64 - k));
}
public:
uint64_t next() noexcept
{
const uint64_t result = s[0] + s[3]; // 4 cycles
const uint64_t t = s[1] << 17; // 3 cycles
s[2] ^= s[0]; // 4 cycles
s[3] ^= s[1]; // 4 cycles
s[1] ^= s[2]; // 4 cycles
s[0] ^= s[3]; // 4 cycles
s[2] ^= t; // 4 cycles
s[3] = rotl(s[3], 45); // 6 cycles
return result;
}
};
//Xoroshiro256+ ma 3 cykle.
class SFC64 {
uint64_t s[4] = { 5, 11, 13, 21 };
static uint64_t rotl(const uint64_t x, int k)
{
return (x << k) | (x >> (64 - k));
}
public:
uint64_t next() noexcept
{
uint64_t tmp = s[0] + s[1] + s[3]++; // 8 cycles
s[0] = s[1] ^ (s[1] >> 11); // 5 cycles
s[1] = s[2] + (s[2] << 3); // 5 cycles
s[2] = rotl(s[2], 24) + tmp; // 8 cycles
return tmp;
}
};
//SFC64 ma 26 cykli.
class PCGDXSM {
__uint128_t LCG = 3311;
public:
uint64_t next() noexcept
{
LCG = LCG * 0xda942042e4dd58b5 + 1; // I assumed 128-bit multiplication is 2 * 4 cycles = 8 cycles, so we have here 13 cycles
uint64_t hi = LCG >> 64; // 5 cycles
uint64_t lo = LCG; // 2 cycles
lo |= 1; // 3 cycles
hi ^= hi >> 32; // 5 cycles
hi *= 0xda942042e4dd58b5ULL; // 6 cycles
hi ^= hi >> 48; // 5 cycles
hi *= lo; // 7 cycles
return hi;
}
};
//PCG-DXSM ma 46 cykli.
class MWC256 {
uint64_t x = 3;
uint64_t y = 33;
uint64_t z = 333;
uint64_t c = 1;
public:
uint64_t next() noexcept
{
const uint64_t result = z; // 2 cycles
const __uint128_t t = 0xfff62cf2ccc0cdaf * (__uint128_t)x + c; // I assumed 128-bit multiplication is 2 * 4 cycles = 8 cycles, so we have here 12 cycles
x = y; // 2 cycles
y = z; // 2 cycles
z = t; // 2 cycles
c = t >> 64; // 5 cycles
return result;
}
};
//MWC-256 ma 25 cykli (możliwe, że zbyt optymistycznie potraktowałem mnożenie liczb 128-bitowych).
class splitmix64 {
uint64_t x = 5;
public:
uint64_t next() noexcept
{
uint64_t z = (x += 0x9e3779b97f4a7c15); // 4 cycles
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; // 9 cycles
z = (z ^ (z >> 27)) * 0x94d049bb133111eb; // 9 cycles
return z ^ (z >> 31); // 4 cycles
}
};
Splitmix64 ma 26 cykli.
class LCG {
uint64_t x = 5;
public:
uint64_t next() noexcept
{
x *= 0xf9b25d65 + 1; // 7 cycles
return x >> 32; // 2 cyles
}
};
//LCG ma 18 cykli (zwraca tylko 32 bity, więc wygenerowanie 64 bitów zajmie mu 2*6 cykli).
Generalnie te szacunki są zbliżone do wyników bez optymalizacji (optim = None). Czy pomiary prędkości tych generatorów bez optymalizacji powinny być zbliżone do ręcznego, zgrubnego oszacowania cykli? Z tego co widzę mniej więcej są, o ile nie walnąłem się gdzieś w zliczaniu operacji. W przypadku MWC256 nie jestem pewien przykładowo, czy x = y; liczyć jako jeden cykl. Zakładam, że mamy do dyspozycji architekturę 64-bitową.
Czy te moje ręczne szacunki mają sens?