"La început a fost numărul" (Pitagora)

Aritmetică cu numere mari

de Ion Cozac

Cum se reprezintă numerele în calculator, cum instruim calculatorul să facă operații aritmetice cu numere mari? Iată câteva întrebări la care încearcă să răspundă articolul de față. Aritmetica numerelor mari are o serie de aplicații, dintre care cea mai cunoscută este criptografia. În final se prezintă tehnica RSA de criptare cu chei publice.

Reprezentarea numerelor în calculator

Un număr întreg se reprezintă în calculator în baza 2 pe unul, doi, patru (uneori opt) octeți. Numărul 2100(10) se reprezintă astfel: 0000100000110100(2) (am folosit reprezentarea pe doi octeți). Reprezentarea numerelor pozitive nu ridică nici un fel de probleme pentru efectuarea operațiilor aritmetice. Domeniul valorilor pozitive care pot fi reprezentate este între 0 și 2N-1, unde N este numărul de biți folosiți pentru reprezentare (8 pentru un octet, 16 pentru doi octeți ș.a.m.d.).

Cum se reprezintă numerele negative? Se pot folosi mai multe metode, fiecare avantajând o categorie de operații aritmetice și dezavantajând pe celelalte; prezentăm aici doar două metode:

  • reprezentarea semnului pe o poziție distinctă; numărul -2100(10) se reprezintă astfel:

-0000100000110100(2);

  • reprezentarea în complement față de doi; un număr negativ m se reprezintă ca fiind rezultatul operației 2N-|m|; reprezentarea numărului -2100(10) se obține astfel:

10000000000000000-

0000100000110100

??????????????????

1111011111001100

Prima metodă de reprezentare complică operațiile de adunare și scădere, pe când operațiile de înmulțire și împărțire se pot efectua după aceleași reguli ca și pentru numere fără semn (evident, la final se va ține cont de regula semnelor pentru rezultat). Domeniul valorilor care pot fi reprezentate este între -2N+1 și 2N-1; pentru numărul zero avem două reprezentări distincte!

A doua metodă (folosită pe majoritatea calculatoarelor) este avantajoasă pentru operațiile de adunare și scădere, care se efectuează ca și pentru numere fără semn. În plus se poate pune în evidență și depășirea capacității de reprezentare a rezultatului. Se contorizează câte transporturi avem în cifra semn și după ea; dacă avem zero sau două transporturi, rezultatul este corect; dacă avem un transport s-a produs depășirea capacității de reprezentare. Operațiile de înmulțire și împărțire se complică, deoarece mai întâi se determină valorile absolute ale operanzilor, apoi se efectuează operația cerută pe operanzi pozitivi, și în final se corectează (eventual) semnul rezultatului. Domeniul valorilor care pot fi reprezentate este între -2N-1 și 2N-1-1.

Reprezentarea numerelor mari

În cele ce urmează prin numere mari înțelegem valori numerice întregi reprezentate pe un număr de biți care depășește capacitatea de prelucrare a unității aritmetice. Notăm cu W capacitatea de memorare (în biți) a unui registru al unității aritmetice (de exemplu 32 pentru microprocesoarele 80386 și mai noi), și cu N numărul de biți folosiți pentru reprezentarea unui număr mare. Considerăm că un număr mare este o valoare numerică reprezentată în baza B=2W iar cifrele numărului sunt memorate în locații succesive de memorie. Cifrele numărului pot fi memorate fie în ordine crescătoare a puterilor bazei B, fie în ordine descrescătoare. În implementarea noastră folosim convenția Intel (ordinea crescătoare); astfel, numărul

X=x0 + x1 B + ... + xn Bn

se memorează astfel începând cu adresa A:

(A)=x0

(A+1)=x1

...

(A+n)=xn

Algoritmii prezentați în continuare consideră că operanzii se reprezintă în format fix pe N biți, și rezultatul se reprezintă tot pe N biți. Situația nu se prezintă întotdeauna astfel; de exemplu, înmulțirea poate produce un rezultat care să se reprezinte pe NŽ2 biți.

Adunarea și scăderea

Operația S:=X+Y se efectuează după următorul algoritm:

carry:=0;

pentru i de la 0 la n executa

(si, carry):=xi + yi + carry;

Am presupus că operația de adunare furnizează atât rezultatul (si) cât și informația dacă avem sau nu transport după cifra semn (carry); această presupunere este valabilă dacă operația este implementată în limbaj de asamblare.

Operația D:=X - Y se efectuează după următorul algoritm:

carry:=0;

pentru i de la 0 la n executa

(di, carry):=xi - yi - carry;

Implementarea pe un calculator cu procesor 80x86 folosește instrucțiuni ADC și SBB (adunare cu transport, scădere cu împrumut).

Pentru efectuarea acestor operații cu numere mari este necesară efectuarea unui număr de N/W operații elementare (ale unității aritmetice).

Înmulțirea

Prezentăm mai întâi o tehnică de înmulțire numită A la russe pe următorul exemplu:

13 x 5=1101 x 101=1000001=65.

1101 101 impar

(11010) 10 par

110100 1 impar

??????????????????????

100001

Pe prima coloană se efectuează înmulțiri succesive cu doi, echivalente cu deplasarea numărului spre stânga cu o poziție (operațiile de deplasare sunt implementate pe aproape orice calculator digital). Pe a doua coloană se efectuează împărțiri succesive la doi, echivalente cu deplasarea numărului spre dreapta cu o poziție. Se adună doar acele valori din prima coloană care corespund unor valori impare din cea de a doua (o valoare impară în baza doi are ultima cifră binară egală cu 1).

Operația P:=XŽY se efectuează după următorul algoritm:

U:=X; V:=Y;

cit timp (V±0) executa:

daca impar(V) atunci P:=P + U;

lshift(U); rshift(V);

Acest algoritm consideră că cei doi operanzi sunt valori binare. Numărul de operații elementare efectuate depinde de mărimea celui de al doilea factor, precum și de numărul de cifre binare din reprezentarea acestuia. Numărul maxim de operații elementare este de (NŽ3/W)Žlog2(V). Acest algoritm poate fi îmbunătățit dacă în prealabil scanăm cei doi factori, și alegem pe post de V pe acela care are ordinul de mărime mai mic (cel mai mic număr de cifre semnificative), și în caz de egalitate pe acela care are cel mai mic număr de cifre binare 1; scanarea se face în NŽ2 pași. O rezolvare mai simplă este de a compara cei doi factori, și de a alege pe post de V pe cel mai mic; comparația se face în cel mult N/W pași. Operația de înmulțire efectuează în cel mai defavorabil caz (NŽNŽ3)/W operații elementare.

Tehnica descrisă mai sus are un ordin de complexitate pătratică în cel mai defavorabil caz. În [Livovschi L, Georgescu H - Sinteza și analiza algoritmilor] este prezentat un algoritm care are un ordin de complexitate O(NŽlog2(N)).

A doua tehnică se bazează pe presupunerea că operația elementară de înmulțire (implementată) furnizează un rezultat pe lungime dublă, partea mai semnificativă (high) și partea mai puțin semnificativă (low), fiecare parte reprezentându-se pe W biți (majoritatea procesoarelor folosesc această implementare). Prezentăm operația de înmulțire simplificată, P:=X Ž v, unde v este o valoare pe care procesorul o poate prelucra direct:

p0:=0;

pentru i de la 0 la n executa:

(high, low):=xi Ž v;

(pi, carry):=pi + low;

daca (i<n) atunci pi+1:=high + carry;

Această tehnică poate fi folosită pentru conversia unui număr din format ASCII (extern) în format binar.

Exercițiu pentru cititori. Implementați mai mulți algoritmi pentru înmulțirea numerelor mari, și analizați practic (cu cronometrul calculatorului) performanțele fiecăruia, pentru diferite valori ale celor doi factori. Dacă W=32, de la ce valoare a lui N algoritmul descris în lucrarea [Livovschi, Georgescu] devine mai eficient decât ceilalți?

Împărțirea

Operația de împărțire în baza 2 imită împărțirea în baza 10, dar este mult mai simplă. Cifrele corecte ale câtului se "ghicesc" mult mai ușor: ele nu pot fi decât 0 sau 1, și această valoare rezultă în urma unei simple comparații. Operația de împărțire furnizează în același timp câtul și restul împărțirii. Trebuie să facem precizarea că în aritmetică împărțirea întreagă se efectuează după următoarele reguli:

  • semnul câtului este dat de regula semnelor;
  • restul este întotdeauna pozitiv sau zero și este mai mic decât valoarea absolută a împărțitorului; operația de împărțire este altfel implementată pe microprocesoarele 80x86 (cu scopul de a simplifica circuitele responsabile): restul are semnul deîmpărțitului.

Înainte de a prezenta algoritmul pentru împărțire să facem câteva precizări:

  • DM (copia deîmpărțitului) se compune din DMH (high) și DML (low);
  • în interiorul ciclului se determină fiecare cifră binară a câtului; rangul acestei cifre este păstrat în variabila rangcit.

Și acum algoritmul pentru împărțire: (C, R):=D / I;

DMH:=0; DML:=D;

C:=0; rangcit:=N;

repeta

rangcit:=rangcit-1;

lshift(DM);

daca (DMHłI) atunci

DMH:=DMH-I;

C (rangcit):=1;

pina cind (rangcit=0);

R:=DMH;

Acest algoritm efectuează în cel mai defavorabil caz (NŽNŽ3)/W operații elementare.

Ca și la înmulțire, se poate elabora un algoritm de împărțire simplificată, care implementează operația (C, r):=D / v, unde:

  • D - deîmpărțitul - este un număr mare;
  • v - împărțitorul - este o valoare pe care procesorul o poate prelucra direct;
  • C - câtul - este un număr mare;
  • r - restul - este o valoare mai mică decât câtul.

Acest algoritm presupune că operația de împărțire elementară (implementată) se execută astfel: deîmpărțitul este reprezentat pe lungime dublă, partea mai semnificativă și partea mai puțin semnificativă, și furnizează la ieșire câtul și restul (majoritatea procesoarelor folosesc această implementare).

high:=0;

pentru i de la 0 la n executa

low:=xi;

(ci, r):=(high, low) / v;

high:=r;

Această tehnică poate fi folosită pentru conversia unui număr din format binar în format ASCII (extern).

Ridicarea la putere

Pentru a calcula Xe folosim reprezentarea lui e în baza 2. Să considerăm calcularea lui X11. Inițial P:=1 și Z:=X. Valorile P și Z evoluează astfel:

P Z E

X X 1011 impar

X3 X2 101 impar

X3 X4 10 par

X11 X8 1 impar

Și acum algoritmul pentru ridicare la putere:

P:=1; Z:=X; F:=E;

cit timp (F±0) executa

daca impar(F) atunci P:=P Ž Z;

Z:=Z Ž Z; rshift(F);

Numărul de înmulțiri (operanzii sunt numere mari!) efectuate este de log2(E) + nr-biti-1(E), deci de maximum 2Žlog2(E).

Remarcă. Algoritmul prezentat în această formă prezintă doar un interes teoretic; rezultatul exponențierii este de multe ori prea mare pentru a putea fi reprezentat în calculator. În schimb prezintă un interes practic deosebit (pentru criptografie) efectuarea operației (XE) mod M. Trebuie să remarcăm faptul că nu este necesară efectuarea operației (XE), și apoi determinarea restului. Se poate folosi următoarea proprietate:

(XE) mod M=((X k) mod M)Ž(X l mod M) mod M, unde k+l=E.

Exerciții pentru cititori. Implementați o funcție care calculează (XE) mod M. Evaluați practic performanțele implementării și reprezentați grafic timpul de răspuns pentru diferite valori ale lui E (valorile X și M rămânând nemodificate). Graficul urmărește alura funcției logaritmice, dar prezintă oscilații în jurul acesteia, deci nu ne indică o funcție strict crescătoare. Care este cauza acestor oscilații? Reprezentați de asemenea și funcția log2(E) + nr-biti-1(E); ce observați?

Inversul multiplicativ

Se dau două numere A și N (A<N) relativ prime (adică cmmdc(A, N)=1). Să se determine o valoare X<N astfel încât (AŽX) mod N=1.

G0:=N; U0:=1; V0:=0;

G1:=A; U1:=0; V1:=1;

cit timp (G1±0) executa:

(Y, G2):=G0 / G1;

(Y citul impartirii, G2 restul)

U2:=U0-Y Ž U1; V2:=V0-Y Ž V1;

U0:=U1; U1:=U2; G0:=G1;

V0:=V1; V1:=V2; G1:=G2;

La sfârșit valoarea V0 este inversul multiplicativ căutat. Pentru a putea implementa acest algoritm folosind numai operanzi pozitivi sunt necesare câteva modificări:

  • operația (U2:=U0-Y Ž U1) se corectează astfel:

Z:=Y Ž U1;

cit timp (U0<Z) executa: U0:=U0 + N;

  • operația (V2:=V0-Y Ž V1) se corectează în mod analog.

Algoritmul RSA

Se determină mai întâi două numere prime mari diferite, P și Q. Se efectuează:

N:=P Ž Q; S:=(P-1) Ž (Q-1);

F este indicatorul lui Euler j(N) și indică câte numere până la N sunt relativ prime cu N.

Se alege un număr E mai mic decât N și relativ prim cu N. Se determină numărul D invers multiplicativ al lui E în raport cu F. Se comunică lumii (se fac publice) valorile N și E, și se ține secretă valoarea D.

Dacă cineva dorește să ne transmită un mesaj M, va proceda astfel:

  • se formează din mesajul M grupe de cifre astfel încât o grupă să fie un număr G mai mic decât M;
  • se calculează C=GE mod N; C este mesajul codificat care se poate transmite.

Decodificarea mesajului se face calculând CE mod N=GDE mod N; această valoare este chiar G, pe baza teoremei lui Euler - Fermat:

Gj(N) mod N=1 pentru orice număr G relativ prim cu N.

Puterea acestei metode se bazează pe faptul că este foarte dificilă factorizarea (descompunerea în factori primi) a unui număr mare N. Pentru informații suplimentare se poate consulta lucrarea [Victor Patriciu - Criptografia și securitatea rețelelor de calculatoare].

Numerele lui Mersenne

În general este foarte dificil de a determina dacă un număr P foarte mare este prim, și de aceea în majoritatea cazurilor se folosesc algoritmi care furnizează informația: "probabilitatea ca P să fie un număr compus este mai mică decât e", unde e este o valoare precizată. Există unele o categorie specială de numere prime care se pot obține pe baza următoarei teoreme:

2n-1 este prim dacă și numai dacă n este prim impar și ln-2=0 unde: l0=4 și lj+1=(lj2-2) mod (2n-1).

Atenție: dacă P și Q sunt numere ale lui Mersenne, atunci produsul P Ž Q se poate descompune foarte ușor.

Implementarea

Operațiile aritmetice elementare (adunare, scădere, înmulțire, împărțire) cu numere mari fără semn au fost implementate într-un fișier OPER.C (pentru Borland C++ 3.1). Prezentăm în continuare interfața cu biblioteca de funcții implementate:

#define LGNR 8

#define LGWO 4

#define LGTN LGNR*LGWO

typedef unsigned long TNumar[LGNR]

void aduna(TNumar *s, TNumar *m, TNumar *n);

void scade(TNumar *d, TNumar *m, TNumar *n);

void multip(TNumar *p, TNumar *m, TNumar *n);

void divide(TNumar *c, TNumar *r, TNumar *m, TNumar *n);

unsigned nenul(Tnumar *n);

int compn(TNumar *m, Tnumar *n);

Explicații:

1. un număr mare se reprezintă pe 8 (LGNR) cuvinte duble de memorie (LGWO=4 octeți) (cu alte cuvinte se folosesc 8 cifre în baza 232 pentru reprezentare);

2. declararea unor variabile de tip "număr mare" se face astfel: Tnumar a,b,c;

3. apelul unei funcții: aduna(&a,&b,&c);

4. se pot declara și punctatori la variabile de tip "număr mare": Tnumar *pa,*pb,*c;

5. apelul unei funcții în acest caz: aduna(pa,pb,pc);

6. sunt implementate următoarele funcții:

  • adunarea: S:=M+N;
  • scăderea: D:=M-N;
  • înmulțirea: P:=MŽN;
  • împărțirea: (C, R):=M/N;
  • test dacă numărul N este nenul; rezultatul este 1 dacă numărul este nenul, și 0 în caz contrar;
  • compararea a două numere M și N; rezultatul este -1 dacă M<N, 0 dacă M=N, 1 dacă M>N;

Acest fișier poate fi inclus într-un proiect pentru Borland C (cu modificari minime aceste funcții pot fi folosite și într-un proiect pentru Borland Pascal). Deoarece unele secvențe sunt scrise în asamblare folosind instrucțiuni 80386, pentru compilare din mediul Borland C este necesară setarea opțiunilor de compilare:

  • Compiler / Advanced Code Generation / Instruction Set 80386 ;
  • Compiler / Code Generation / Compile via Assembler .

Pentru compilare cu BCC (linie de comandă) se folosesc comenzile:

BCC-3-S oper.c

TASM oper

Ion Cozac este lector la Universitatea "Petru Maior" din Târgu-Mureș și poate fi contactat prin e-mail la adresa cozac@uttgm.ro

[cuprins]