Come altri hanno sottolineato, tutti gli aritmetici a 64 bit nell'esempio sono stati ottimizzati. Questa risposta si concentra sulla domanda nel titolo.
Fondamentalmente trattiamo ciascun numero a 32 bit come una cifra e lavoriamo nella base 4294967296.In questo modo possiamo lavorare su grandi numeri arbitrariamente.
L'addizione e la sottrazione sono più facili. Lavoriamo attraverso le cifre una alla volta partendo dal meno significativo e passando al più significativo. Generalmente la prima cifra viene eseguita con una normale istruzione di addizione/sottrazione e le cifre successive vengono eseguite utilizzando una specifica istruzione "aggiungi con trasporta" o "sottrai con un prestito". Il flag carry nel registro di stato viene usato per prendere il bit carry/borrow da una cifra alla successiva. Grazie a due complementi di addizione e sottrazione firmati e non firmati sono gli stessi.
La moltiplicazione è un po 'più complicata, moltiplicando due cifre a 32 bit è possibile ottenere un risultato a 64 bit. La maggior parte dei processori a 32 bit avrà istruzioni che moltiplicano due numeri a 32 bit e produce un risultato a 64 bit in due registri. Sarà quindi necessaria l'aggiunta per combinare i risultati in una risposta finale. Grazie a due complementi, la moltiplicazione con segno e senza segno è la stessa a condizione che la dimensione del risultato desiderata sia la stessa della dimensione dell'argomento. Se il risultato è maggiore degli argomenti, è necessario prestare particolare attenzione.
Per il confronto iniziamo dalla cifra più significativa. Se è uguale, passiamo alla cifra successiva finché i risultati non sono uguali.
La divisione è troppo complessa per essere descritta in questo post, ma ci sono molti esempi di algoritmi. per esempio. http://www.hackersdelight.org/hdcodetxt/divDouble.c.txt
Alcuni esempi reali da gcc https://godbolt.org/g/NclqXC, l'assemblatore è nella sintassi intel.
Prima un'aggiunta. aggiunta di due numeri a 64 bit e produzione di un risultato a 64 bit. Asm è lo stesso per entrambe le versioni firmate e non firmate.
int64_t add64(int64_t a, int64_t b) { return a + b; }
add64:
mov eax, DWORD PTR [esp+12]
mov edx, DWORD PTR [esp+16]
add eax, DWORD PTR [esp+4]
adc edx, DWORD PTR [esp+8]
ret
Questo è piuttosto semplice, caricare un argomento in eax e edx, quindi aggiungere l'altro utilizzando un componente aggiuntivo seguito da un aggiungere con riporto. Il risultato è lasciato in eax ed edx per tornare al chiamante.
Ora una moltiplicazione di due numeri a 64 bit per produrre un risultato a 64 bit. Di nuovo il codice non cambia da firmato a non firmato. Ho aggiunto alcuni commenti per renderlo più facile da seguire.
Prima di esaminare il codice, prendere in considerazione la matematica. a e b sono numeri a 64 bit Userò lo() per rappresentare i 32 bit inferiori di un numero a 64 bit e hi() per rappresentare i 32 bit superiori di un numero a 64 bit.
(a * b) = (lo (a) * lo (b)) + (ciao (a) * lo (b) * 2^32) + (ciao (b) * lo (a) * 2^32) + (ciao (b) * ciao (a) * 2^64)
(a * b) mod 2^64 = (lo (a) * lo (b)) + (lo (ciao (ciao a) * lo (b)) * 2^32) + (lo (hi (b) * lo (a)) * 2^32)
lo ((a * b) mod 2^64) = lo (lo (a) * lo (b))
hi ((a * b) mod 2^64) = hi (lo (a) * lo (b)) + lo (hi (a) * lo (b)) + lo (hi (b) * lo (a))
uint64_t mul64(uint64_t a, uint64_t b) { return a*b; }
mul64:
push ebx ;save ebx
mov eax, DWORD PTR [esp+8] ;load lo(a) into eax
mov ebx, DWORD PTR [esp+16] ;load lo(b) into ebx
mov ecx, DWORD PTR [esp+12] ;load hi(a) into ecx
mov edx, DWORD PTR [esp+20] ;load hi(b) into edx
imul ecx, ebx ;ecx = lo(hi(a) * lo(b))
imul edx, eax ;edx = lo(hi(b) * lo(a))
add ecx, edx ;ecx = lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
mul ebx ;eax = lo(low(a) * lo(b))
;edx = hi(low(a) * lo(b))
pop ebx ;restore ebx.
add edx, ecx ;edx = hi(low(a) * lo(b)) + lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
ret
Finalmente quando proviamo una divisione vediamo.
int64_t div64(int64_t a, int64_t b) { return a/b; }
div64:
sub esp, 12
push DWORD PTR [esp+28]
push DWORD PTR [esp+28]
push DWORD PTR [esp+28]
push DWORD PTR [esp+28]
call __divdi3
add esp, 28
ret
Il compilatore ha deciso che la divisione è troppo complessa da implementare in linea e chiama una routine di libreria, invece.
L'uso di 'pow' è un modo molto pessimo e soggetto a errori per calcolare i poteri interi di 2. Non usarlo. Nota che se la tua sottrazione a virgola mobile non viene eseguita come 'double double 'a 80-bit,' pow (2,64) -1' sarebbe uguale a 'pow (2,64)', e quindi lo getti in 'unsigned long long' non funzionerebbe correttamente. –