Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Ile cykli zajmuje mnożenie liczb 64-bitowych?

64 views
Skip to first unread message

osobli...@gmail.com

unread,
May 11, 2023, 10:28:09 AM5/11/23
to
Znalazłem dwa źródła:

http://www.phys.ufl.edu/~coldwell/MultiplePrecision/fpvsintmult.htm

https://stackoverflow.com/questions/21819682/is-integer-multiplication-really-done-at-the-same-speed-as-addition-on-a-modern

W jednym piszą, że to jest 20 cykli. W drugim 2-4 cykle (dla liczb 32-bitowych, dla 64-bitowych będzie dwa razy więcej?). Chcę zgrubnie oszacować liczbę cykli przypadającą na dwa różne algorytmy. Na przykład:

https://prng.di.unimi.it/xoroshiro128plusplus.c

Ale w drugim algorytmie mam mnożenie dwóch uint64_t. I nie wiem ile cykli mniej więcej przyjąć.

Bogdan

unread,
May 13, 2023, 10:10:00 AM5/13/23
to
Być może to nieoczywiste, ale jaka architektura? Na amd64 mnożenie
liczb 64-bitowych (czyli o wielkości rejestru) będzie zapewne o wiele
szybsze, niż na systemach 32-bitowych, o 16-bitowych nie wspominając.

We floating point może bym nie szedł, bo może być utrata precyzji, no
i trzeba konwertować.

Tak czy siak, pierwszy link z wyszukiwarki zapytanej o "intel
instruction latencies", zakładając, że jednak chodzi o architekturę
x86/x64: www.agner.org/optimize/instruction_tables.pdf.
Wybieram losowo procesor Haskell: MUL: czas: 3-4 cykli,
przepustowość: 0,5-1 instrukcji na cykl.

Najlepiej pobierz dokument i wybierz stosowny procesor.


--
Pozdrawiam/Regards - Bogdan (GNU/Linux & FreeDOS)
Kurs asemblera x86 (DOS, GNU/Linux): http://bogdro.evai.pl
Grupy dyskusyjne o asm: pl.comp.lang.asm alt.pl.asm alt.pl.asm.win32
www.Xiph.org www.TorProject.org Soft(EN): http://bogdro.evai.pl/soft

osobli...@gmail.com

unread,
May 13, 2023, 1:28:57 PM5/13/23
to
Interesuje mnie głównie architektura 64-bitowa, ale też GPU. Co do tabeli - te 3-4 cykle to jest dla liczb 32-bitowych? Mam zakładać, że dla 64-bitowych będzie to samo, czy dwa razy więcej?

osobli...@gmail.com

unread,
May 13, 2023, 1:34:34 PM5/13/23
to
Ok, widzę, że dla 64-bitowych liczb to też jest 4-5 cykli.

osobli...@gmail.com

unread,
May 13, 2023, 1:43:33 PM5/13/23
to
Swoją drogą mierzę sobie względną szybkość generatorów PRNG za pomocą:

https://quick-bench.com

Jedyne sensowne zestawienie, po zliczeniu przez mnie ręcznie liczby cykli na operacje (wynik 22 do 13), które wykonują algorytmy, dostaję, gdy włączam optim=None. Jeżeli zaś włączę OFast xoroshiro dostaje takiego przyspieszenia, że wyprzedza drugi PRNG, według tego benchmarku.

W samym xoroshiro liczę operację:

const uint64_t s0 = s[0];

jako jeden cykl, bo następuje wywołanie zmiennej z tablicy. Ale nie jestem pewien, czy to tak należy szacować.

Bogdan

unread,
May 14, 2023, 5:28:17 AM5/14/23
to
To zależy od poziomu optymalizacji.
Bez optymalizacji na samo to wziąłbym 1 cykl na kopię z pamięci do
rejestru i 1 na kopię z rejestru do innej pamięci. Ale wspomniany
dokument podaje np. 3 cykle na kopiowanie do pamięci, więc nawet to
nie jest takie oczywiste.
Z optymalizacją jest szansa, że "s0" siedzi w rejestrze, więc
wystarczy pewnie 1 cykl na załadowanie.
Oczywiście, jeśli s[0] jest ułożone na równym adresie.
Oczywiście, jeśli s[0] siedzi w cache, bo jeśli nie, to w najgorszym
przypadku mogą być może dziesiątki, jak nie setki cykli na pobranie z
głównej pamięci.
I pewnie jeszcze różne inne warunki, więc tabelki tabelkami, ale
najlepiej albo pomierzyć (RDTSC), albo użyć narzędzi mówiących, co ile
potrwa (kiedyś było np. jakieś VTune Analyzer).

osobli...@gmail.com

unread,
May 14, 2023, 10:00:11 AM5/14/23
to
Ok, czyli liczę to raczej prawidłowo. Przykładowe szacunki:

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]; // 3 cycles

const uint64_t t = s[1] << 17; // 2 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; // 2 cycles

s[3] = rotl(s[3], 45); // 6 cycles

return result;
}
};

//Xoroshiro256+ ma 29 cykli.

osobli...@gmail.com

unread,
May 14, 2023, 10:39:05 AM5/14/23
to
niedziela, 14 maja 2023 o 11:28:17 UTC+2 Bogdan napisał(a):
> On 13/05/2023 19:43, osobli...@gmail.com wrote:
> > Swoją drogą mierzę sobie względną szybkość generatorów PRNG za pomocą:
> >
> > https://quick-bench.com
> >
> > Jedyne sensowne zestawienie, po zliczeniu przez mnie ręcznie liczby cykli na operacje (wynik 22 do 13), które wykonują algorytmy, dostaję, gdy włączam optim=None. Jeżeli zaś włączę OFast xoroshiro dostaje takiego przyspieszenia, że wyprzedza drugi PRNG, według tego benchmarku.
> >
> > W samym xoroshiro liczę operację:
> >
> > const uint64_t s0 = s[0];
> >
> > jako jeden cykl, bo następuje wywołanie zmiennej z tablicy. Ale nie jestem pewien, czy to tak należy szacować.
> To zależy od poziomu optymalizacji.
> Bez optymalizacji na samo to wziąłbym 1 cykl na kopię z pamięci do
> rejestru i 1 na kopię z rejestru do innej pamięci.

To jest to samo co niejakie load/store time? Jeżeli w algorytmie mam:

k = k + x;

To dobrze rozumiem, że mam liczyć to jako 4 cykle? Bo jeden cykl na pobranie k, drugi cykl na pobranie x, trzeci cykl na dodawanie i czwarty cykl na przypisanie wyniku do k?

Bogdan

unread,
May 15, 2023, 8:03:09 AM5/15/23
to
Jak już pisałem - to może zależeć od konkretnego modelu procesora...
Nie tylko od tego, że jest 64-bitowy. I od poziomu optymalizacji.

result = s[0] + s[3];
// jeśli result idzie do pamięci
// mov + mov + add + mov = 2+2+1+3
// mov + add + mov = 2+6+3
// jeśli result idzie do rejestru
// mov + mov + add = 2+2+1
// mov + add = 2+6

const uint64_t t = s[1] << 17;
// jeśli t idzie do pamięci
// mov + shl + mov = 2+1+3
// jeśli t idzie do rejestru
// mov + shl = 2+1

I tak dalej...

Bogdan

unread,
May 15, 2023, 8:03:16 AM5/15/23
to
Zależy od konkretnego modelu procesora i od poziomu optymalizacji. I
od rozmiaru zmiennych, i od cache, i od ułożenia w pamięci.
Jeśli 'k' jest 64-bitowe, to:

1) MOV 'k' z pamięci do rejestru = 2, MOV 'x' z pamięci do rejestru =
2, ADD = 1, potem ewentualne czekanie na wynik, potem MOV nowej
wartości do pamięci = 3.

2) MOV 'x' z pamięci do rejestru = 2, ADD 'x' do 'k' w pamięci z
rejestru = 6.

Najlepiej mierzyć fizycznie, co jest najszybsze.
Jak mierzenie jest trudne, to można chociaż zobaczyć, jakie
instrukcje kompilator generuje (z różnymi poziomami optymalizacji i
wyboru architektury docelowej) i dopiero mając je, wziąć tabele czasów
instrukcji i liczyć.

osobli...@gmail.com

unread,
May 15, 2023, 9:10:18 AM5/15/23
to
A gdyby użyć tego:

https://godbolt.org

Jak wrzucam sobie tam kod:

#include <cstdint>

inline uint64_t rotl(const uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}


uint64_t s[2] = {1,2};

uint64_t next(void) {
const uint64_t s0 = s[0];
uint64_t s1 = s[1];
const uint64_t result = s0 + s1;

s1 ^= s0;
s[0] = rotl(s0, 24) ^ s1 ^ (s1 << 16); // a, b
s[1] = rotl(s1, 37); // c

return result;
}

To ma sens analiza tego:

movq s(%rip), %rax
movq s+8(%rip), %rsi
movq %rax, %rdx
movq %rax, %rcx
addq %rsi, %rax
xorq %rsi, %rdx
rolq $24, %rcx
movq %rdx, %rdi
xorq %rdx, %rcx
rorq $27, %rdx
salq $16, %rdi
movq %rdx, s+8(%rip)
xorq %rdi, %rcx
movq %rcx, s(%rip)
ret

Jak rozumiem wszystko to są standardowe nazwy instrukcji? Więc wystarczy zsumować ich cykle? Mogę założyć, że movq to 2 cykle, xorq to 1 cykl, salq to 1 cykl?

osobli...@gmail.com

unread,
May 15, 2023, 12:02:33 PM5/15/23
to
Swoją drogą jak biorę wartości z tych tabel:

https://www.agner.org/optimize/instruction_tables.pdf

Mam brać pod uwagę sumę Ops i Latency?

Wojciech Muła

unread,
May 22, 2023, 1:30:31 PM5/22/23
to
Pytałeś już o to dwa lata temu. I nie, ops i latency to dwie **kompletnie** różne rzeczy - przeczytaj dokładnie opisy kolumn.

Można teoretycznie oszacować throughput małego kawałka kodu (np. na https://uica.uops.info), można nawet oszacować dolne ograniczenie latency ze ścieżki krytycznej. Między Tobą a ISA jest kompilator, runtime, system operacyjny i wg mnie warto mierzyć czas wykonania dla dużej liczby iteracji, eksprymentując z ustawieniami kompilatora. Od wielu lat używam tego zestawu makr do mierzenia cykli: https://github.com/WojciechMula/toys/blob/master/000helpers/benchmark.h. Możesz zobaczyć w tym repo jak z tego korzystać, np. tutaj: https://github.com/WojciechMula/toys/blob/master/avx512-varuint/benchmark.cpp#L41

w.

osobli...@gmail.com

unread,
Jun 2, 2023, 5:01:56 AM6/2/23
to
poniedziałek, 22 maja 2023 o 19:30:31 UTC+2 Wojciech Muła napisał(a):
> On Monday, May 15, 2023 at 6:02:33 PM UTC+2, osobli...@gmail.com wrote:
> > Swoją drogą jak biorę wartości z tych tabel:
> >
> > https://www.agner.org/optimize/instruction_tables.pdf
> >
> > Mam brać pod uwagę sumę Ops i Latency?
> Pytałeś już o to dwa lata temu. I nie, ops i latency to dwie **kompletnie** różne rzeczy - przeczytaj dokładnie opisy kolumn.

Fakt, wtedy chodziło mi tylko o rzędy wielkości, bo nie miałem pojęcia ile to w ogóle zajmuje. Dzisiaj porównuję mój kod z różnymi wersjami xoroshiro:

https://prng.di.unimi.it/xoshiro256plusplus.c

> Można teoretycznie oszacować throughput małego kawałka kodu (np. na https://uica.uops.info), można nawet oszacować dolne ograniczenie latency ze ścieżki krytycznej.

Nie wiem, czy czegoś podobnego nie robi Godbolt:

https://godbolt.org/z/7rxMdKerz

[1] [2] [3] [4] [5] [6] Instructions:
1 5 0.50 * movq _ZL1s.0(%rip), %rcx
1 5 0.50 * movq _ZL1s.3(%rip), %rdx
1 1 0.50 leaq (%rdx,%rcx), %rax
1 1 0.50 rolq $23, %rax
1 1 0.25 addq %rcx, %rax
1 5 0.50 * movq _ZL1s.1(%rip), %rsi
1 1 0.25 movq %rsi, %rdi
1 1 0.50 shlq $17, %rdi
1 5 0.50 * movq _ZL1s.2(%rip), %r8
1 1 0.25 xorq %rcx, %r8
1 1 0.25 xorq %rsi, %rdx
1 1 0.25 xorq %r8, %rsi
1 1 1.00 * movq %rsi, _ZL1s.1(%rip)
1 1 0.25 xorq %rdx, %rcx
1 1 1.00 * movq %rcx, _ZL1s.0(%rip)
1 1 0.25 xorq %rdi, %r8
1 1 1.00 * movq %r8, _ZL1s.2(%rip)
1 1 0.50 rolq $45, %rdx
1 1 1.00 * movq %rdx, _ZL1s.3(%rip)
3 7 1.00 U retq

> Między Tobą a ISA jest kompilator, runtime, system operacyjny i wg mnie warto mierzyć czas wykonania dla dużej liczby iteracji, eksprymentując z ustawieniami kompilatora.

Tak to obecnie mierzę. Kompiluję z takimi flagami g++ -o test benchmarks_PRNGs.cpp -Wall -Wextra -O3 -fno-unroll-loops. No i coś tam mi wychodzi. Wyniki różnią się od tych z Godbolt. Wydaje mi się, że ze względu na mnożenie w moich generatorze niewiele tutaj można zrobić (przyspieszyć). Tym bardziej, że, jeśli porównuję wyniki xoroshiro i PCG-DXSM:

static __uint128_t LCG_state = 123456789;

uint64_t next_PCG(void)
{
LCG_state = LCG_state * 0xda942042e4dd58b5 + 1;
uint64_t hi = LCG_state >> 64;
uint64_t lo = LCG_state;

lo |= 1;
hi ^= hi >> 32;
hi *= 0xda942042e4dd58b5ULL;
hi ^= hi >> 48;
hi *= lo;
return hi;
}

To dostaję podobne wyniki do prof. Vigny https://prng.di.unimi.it. PCG jest jakieś 1,5 razy wolniejszy. Zastanawiam się jeszcze tylko, czy mnożenie można jakoś zoptymalizować. Twórczyni PCG Melissa O'Neil pisała gdzieś, że w jej pomiarach PCG jest tylko nieznacznie wolniejszy od xoroshiro. Tutaj:

https://github.com/numpy/numpy/pull/13163#issuecomment-496030958

wyszło komuś z Numpy, że na Linuxie PCG jest tylko nieznacznie wolniejszy od xoroshiro, a na Windowsie różnica jest ponad dwukrotna. Prędkość zależna od platformy?

> Od wielu lat używam tego zestawu makr do mierzenia cykli: https://github.com/WojciechMula/toys/blob/master/000helpers/benchmark.h. Możesz zobaczyć w tym repo jak z tego korzystać, np. tutaj: https://github.com/WojciechMula/toys/blob/master/avx512-varuint/benchmark.cpp#L41

Dzięki, ale nie wiem jak tego użyć.

osobli...@gmail.com

unread,
Jun 2, 2023, 8:11:43 AM6/2/23
to
Tutaj Vignie wyszły podobne wyniki przy użyciu Intel® Architecture Code Analyzer, 3.50 cykla dla xoroshiro128++ i 5.53 cykla dla PCG64DXSM:

https://pcg.di.unimi.it/pcg.php

Także, jeśli uzyskuję podobne wyniki, chyba robię pomiary w miarę reprezentatywnie. I chyba niewiele można tu przyspieszyć. Nie rozumiem tylko dlaczego ludziom z Numpy wyszły wyrównane wyniki i Melissie O'Neil też takie ponoć wychodzą.
0 new messages