Puncte:2

O modalitate eficientă de a alege un index de matrice utilizând, să zicem, un număr aleator de 64 de biți?

drapel in

Spune, am uint64_t rand = <un număr aleatoriu>, și matrice de caractere[20] =.... Scopul meu este să aleg un element matrice pe baza continutului de rand.

  1. O modalitate lentă este să folosești restul: size_t i = rand % 20 apoi alegeți elementul de matrice[i].
  2. Un alt mod, care Cred este mai rapid, este i = rand/UINT64_MAX * 20. Sau, pentru a evita necesitatea operațiunilor flotante, partea sa inversă 20/(UINT64_MAX/rand).
  3. O a treia modalitate este de a folosi biții aleatori pentru a se ramifica la index ca un copac (dar ratează fiecare al 5-lea număr):
size_t total_bytes = 20;
size_t mask = 1;
dimensiunea_t i = 0;
while (total_bytes) {
  if (rand & mask) i += total_bytes / 2; // ramură dreapta
  altfel i += 0; // ramură stângă
  masca <<= 1;
  total_octeți /= 2;
}

Există vreo modalitate mai rapidă pe hardware-ul comun? De exemplu. PC-uri laptop/desktop?

Motivul pentru care îmi pasă: implementez o funcție de derivare a tastei de memorie și, la un moment dat, trebuie să aleg un element de matrice bazat pe conținutul textului cifrat calculat. Numărul aleatoriu este de 64 de biți.

Limba țintă este C.

Meir Maor avatar
drapel in
Ai verificat că %20 este prea lent? Pe un PC modern? aș fi șocat.
Maarten Bodewes avatar
drapel in
@caveman Nu contează, întrebarea a fost puțin diferită de cea de așteptat. Comentarii tarziu....
drapel in
Cross postat: https://stackoverflow.com/questions/68809491/whats-the-fastest-method-in-c-for-converting-a-64bit-random-number-into-a-small cu mai multe detalii în comentarii , inclusiv că „20” nu este o constantă.
Puncte:4
drapel ng

rand % 20 generează un rezultat în $\{0,1,\ldots,18,19\}$ acesta este aproape uniformă (presupunând rand este): $\Pr(19)/\Pr(0)=1-1/922337203685477581$. Aceasta este adesea o părtinire tolerabilă.

Pe un „laptop/desktop PC” cu un procesor modern pe 64 de biți, rand % 20 este destul de rapid și are virtuțile importante de a fi corect, simplu și ușor de adaptat. Cu toate acestea, este cel puțin des (vezi cometariu) posibil să fie mai rapid folosind

(rand-((rand-(rand>>2))>>1))>>59

care are același raport (optim) între cele mai puțin și cele mai probabile rezultate, utilizând în același timp doar operațiunile de schimbare și adăugare. Sunt mai încrezător că codul generat este în timp constant, ceea ce poate fi important în aplicațiile cripto. Și media este mai aproape de $19/2$.

Pentru o intuiție a modului în care funcționează această formulă: pentru orice $x\in\mathbb R$ tine $(x-(x-x\,2^{-2})\,2^{-1})\,2^{-59}=20\,x\,2^{-64}$, astfel evaluăm în mod esențial care sunt expresiile (uint64_t)floor(rand*(20/(UINT64_MAX+1.))) sau (uint64_t)((rand*(uint128_t)20)>>64) încercarea de a evalua. Observați că pentru unele valori, inclusiv rand=0xCCCCCCCCCCCCCCCCCC formula ulterioară nu coincide tocmai cu formula pe care o propun; totuși distribuția realizată de ambii este optim uniformă.

Metoda nu se limitează la constantă $m=20$ pentru dimensiunea matricei. Se generalizează la oricare constant $m$ cu greutate Hamming moderată. Calcularea numărătorilor de deplasări adecvate din constante este netrivială. Ma refer la asta răspuns minunat (notă: numărul ultimului schimb dat acolo trebuie crescut cu 32 în cazul în cauză) pentru ceva care funcționează, dar nu este întotdeauna optim. Nu am altă referință pentru metoda, pe care am (re-?)inventat-o ​​pentru un ARM Cortex-M0, unde s-a dovedit utilă. De fapt, am găsit doar empiric formule pentru câteva constante care se potrivesc nevoilor mele, iar Anders Kaseorg își asumă toată meritul pentru modul de a genera formule în mod sistematic.


Dacă suntem dispuși să pierdem puțină uniformitate și siguranța că codul este în timp constant, putem folosi

((rand>>3)*5)>>59

care este mai simplu, probabil mai rapid și mai ușor de adaptat la alte constante $m$ Decat $20$: noi scriem $m$ la fel de $r\,2^i$ cu $i$ un număr întreg și $r$ de preferință impar, apoi găsiți numărul întreg $j$ cu $2^{j-1}\le r<2^j$. Folosim ((rand>>j)*r)>>(64+i-j). Problema este, mai jos $j$ bucăți de rand nu sunt utilizate, iar uniformitatea rezultatului este redusă în mod corespunzător (cu excepția cazului în care $m$ este o putere a doi).

Când $m$ este $2^j$ pentru un număr întreg $j$, putem folosi rand>>(64-j) sau rand&(m-1). Cel mai târziu este observat în celălalt răspuns. Aceste metode nu pierd nicio uniformitate, dacă toate părțile rand sunt uniforme și independente.

Dacă $m$ modificări în timpul rulării cu $m<2^j$ pentru o constantă cunoscută $j$, putem folosi

((rand>>j)*m)>>(64-j)

Însă $j$ biți mai mici de rand sunt pierdute și care reduce uniformitatea rezultatului (cu excepția cazului în care $m$ este o putere a doi).


Pe langa subiect:

  • (uint64_t)(etaj (rand*(20/(UINT64_MAX+1.)))) ar fi OK dacă nu ar exista o eroare de rotunjire, dar pentru că acestea există, este greu de spus dacă poate da rezultate 20 pentru unele intrări; de asemenea, pe multe compilatoare nu este uniform uniform.
  • (uint64_t)((rand*(uint128_t)20)>>64) este corect din punct de vedere matematic și foarte aproape de ceea ce evaluăm noi, dar uint128_t este o caracteristică C opțională și încă suportată marginal.
  • Întrebarea e rand/UINT64_MAX * 20 iesiri in $\{0,20\}$ deci este nepotrivit. Problemele sunt împărțirea rotunjite în jos la întreg și (independent) asta rand poate fi UINT64_MAX.
  • Întrebarea e 20/(UINT64_MAX/rand) iesiri in $\{0,1,2,3,4,5,6,10,20\}$ și poate provoca o împărțire la zero, deci este nepotrivit. Problemele sunt împărțirea rotunjite în jos la întreg și (independent) asta rand poate fi 0.
  • Fragmentul de cod al întrebării 3 are întotdeauna i%5 != 4 la ieșire, deci este nepotrivit. Problema este că ieșirea i este construit ca 10+5+2+1 cu unii termeni eliminati.
Gilles 'SO- stop being evil' avatar
drapel cn
La optimizarea vitezei pe un CPU obișnuit pe 64 de biți, restul sau împărțirea printr-o constantă este compilată la o înmulțire cu o constantă plus unele deplasări și adunări/scăderi. Divizarea hardware-ului este lentă și compilatorii știu acest lucru (deși majoritatea nu vor face matematica timpului de compilare pentru o diviziune pe 64 de biți pe un procesor pe 32 de biți).Schimbările pe care le propui au aproximativ același număr de instrucțiuni, dar nicio multiplicare și același număr de accesări la memorie, așa că este foarte probabil ca metoda ta de schimbare să fie mai rapidă pe orice CPU, cu excepția unora concepute pentru timp real cu mul cu număr redus de cicluri. /div. https://godbolt.org/z/z4PverffY
fgrieu avatar
drapel ng
@Gilles'SO-stopbeingevil': nu am reușit să găsesc informațiile potrivite în [acea mizerie](https://software.intel.com/content/dam/develop/external/us/en/documents-tps/325462-sdm -vol-1-2abcd-3abcd.pdf) pentru a confirma că optimizarea pe care o menționezi încă merită pe cele mai recente procesoare x64. Actualizare: sunt indicat [aceste](https://www.agner.org/optimize/#manuals) resurse utile.
Gilles 'SO- stop being evil' avatar
drapel cn
Cred că trebuie să găsești un manual specific modelului pentru asta. Te-ai conectat la referința arhitecturii generice. Referința setului de instrucțiuni (volumul 2) ar fi mai relevantă, dar chiar și aceasta este doar o descriere funcțională, nu include numărătoare de cicluri (care nu spun povestea completă a performanței, dar pentru acest caz simplu nu există ramificare sau paralelism deci cred că adăugarea numărului de cicluri ar duce la o comparație semnificativă).
caveman avatar
drapel in
Ar merita să generalizăm acea soluție de schimbare la orice număr, altul decât 20, pentru a obține mai puține cicluri decât utilizarea abordării `%`? Pentru că 20 nu este o constantă, ci un simplu exemplu pe care l-am ales.
fgrieu avatar
drapel ng
@caveman: răspunsul clarifică acum că da, putem extinde și la alte constante. [Acest lucru](https://tinyurl.com/unicst) oferă formule pentru toate constantele cu până la 3 cifre zecimale (dar asigurați-vă că adăugați 32 la ultimul număr de schimburi). Din nou, această optimizare are sens numai dacă operatorul `%` este lent și nu va fi pe un laptop/desktop PC-uri moderne.
Gilles 'SO- stop being evil' avatar
drapel cn
@caveman Nu sunt un expert, dar cred că în ceea ce privește performanța, calculele necesare pentru a calcula schimburile necesare vor costa mai mult de o instrucție de divizie. Cu toate acestea, abordarea cu schimburi are alte beneficii decât performanța, în cea mai mare parte fiind garantată că nu va avea un timp care depinde de datele secrete.
drapel pe
Aceasta pare o versiune mai complicată a [Lemire](https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/) `(rand() * 20) >> 64` abordare.
fgrieu avatar
drapel ng
@SamuelNeves: sunt diferențe. (A) Expresia `(rand() * 20) >> 64` necesită evaluarea produsului pe 69 de biți, iar acest lucru nu este posibil portabil; trucul Lemire legat este cu `rand()` pe 32 de biți extins la 64 de biți și lovește acel perete pentru `rand()` pe 64 de biți. (B) Pentru unele valori ale lui `rand()`, inclusiv 0xCCCCCCCCCCCCCCCC, ceea ce propun diferă cu unul, dar are totuși o distribuție ideală uniformă.
Puncte:3
drapel in

Fă doar % 20

Conform http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/ Diviziunea întregului nu costă 12-44 cicluri CPU pe un procesor modern (și, în unele cazuri, mai puțin datorită structurii conductei dacă ALU nu face nimic altceva) Având în vedere următorul lucru pe care doriți să-l faceți este un acces la memorie, care în cel mai bun caz va fi o citire L1, va costa 3-4 cicluri în sine și probabil că doriți să faceți ceva cu această valoare.

Nu-mi pot imagina un scenariu în care merită optimizat, chiar dacă este posibil să se reducă un timp sau două.

Căutați blocajele înainte de optimizare.

fgrieu avatar
drapel ng
[Imaginea](http://ithare.com/wp-content/uploads/part101_infographics_v08.png) din sursa dvs. utilă afirmă că diviziunea întregului costă 15-40 de cicluri. Textul a citat o referință ca dând „costul diviziunii pe 32/64 de biți (cunoscut sub numele de DIV/IDIV pe x86/64) – între 12-44 de cicluri”. Din experiența mea, asta este extrem de dependent de platformă și de lățimea argumentelor, iar intuiția mea este că 15 sau chiar 12 nu reflectă marginea 2021. Intuiția noastră inițială (împărtășită) că pe un procesor x64 `i%20` este suficient de rapid și ar putea fi cel mai rapid mai are sens.
Meir Maor avatar
drapel in
@fgrieu Într-adevăr, am copiat numărul greșit, am corectat numărul. Nu schimbă linia de jos. Acest lucru este rapid.
Gilles 'SO- stop being evil' avatar
drapel cn
Dacă 20 este o constantă și numerele nu sunt mai mari decât un cuvânt de mașină, `% 20` va fi de obicei optimizat pentru o înmulțire, care necesită mai puține cicluri decât o împărțire, reducând și mai mult diferența. În orice caz, sunt de acord că chiar și o diviziune este neglijabilă în comparație cu accesările la memorie pe orice platformă cu un cache de memorie (mai ales dacă este o căutare a tabelului de timp constant care necesită multe încărcări). Cu toate acestea, pentru aplicațiile criptografice, poate fi de nedorit să se utilizeze diviziunea sau înmulțirea, deoarece este obișnuit ca acestea să aibă o temporizare dependentă de date.
Meir Maor avatar
drapel in
Inițial am dat numărul de cicluri pentru înmulțire și apoi am editat următorul comentariu. Microoptimizarea reală ca aceasta este dificilă și depinde de ce altceva se întâmplă pentru a vedea cât de bine împachetează procesorul instrucțiunile. Deși cred că nu-mi voi face răspunsul mai lung decât este.
Puncte:1
drapel sk

De obicei, s-ar strădui să facă dimensiunea matricei o putere de 2. Apoi indicele poate fi calculat prin ȘI pe biți:

matrice de caractere[0x40];
uint64_t rand;
...
char c = matrice[rand & 0x3f];
drapel id
Acesta este un fel de răspuns „Pot rezolva o altă problemă foarte repede”. Sigur, dar nu aceasta este întrebarea pusă. Și în cripto, când algoritmul spune să folosiți 20, nu înlocuiți 32 doar pentru că ar fi mai rapid. Genul ăsta de programare este modul în care distrugi cripto.
ThomasM avatar
drapel sk
După cum am înțeles întrebarea, algoritmul nu este dat, ci în construcție. În caz contrar, ar exista probabil o modalitate determinată de a calcula indicele din numărul aleatoriu și nu s-ar putea încerca diferite metode pentru a găsi cea mai rapidă.

Postează un răspuns

Majoritatea oamenilor nu înțeleg că a pune multe întrebări deblochează învățarea și îmbunătățește legătura interpersonală. În studiile lui Alison, de exemplu, deși oamenii își puteau aminti cu exactitate câte întrebări au fost puse în conversațiile lor, ei nu au intuit legătura dintre întrebări și apreciere. În patru studii, în care participanții au fost implicați în conversații ei înșiși sau au citit transcrieri ale conversațiilor altora, oamenii au avut tendința să nu realizeze că întrebarea ar influența – sau ar fi influențat – nivelul de prietenie dintre conversatori.