Per fornire forse un esempio più chiaro, su x86_64, compilato con il -O
flag, la funzione
pub fn leet(a : i128) -> i128 {
a + 1337
}
compila a
example::leet:
mov rdx, rsi
mov rax, rdi
add rax, 1337
adc rdx, 0
ret
(Il mio post originale aveva u128
piuttosto che il i128
tuo chiesto. La funzione compila lo stesso codice in entrambi i casi, una buona dimostrazione che l'aggiunta firmata e non firmata sono le stesse su una CPU moderna.)
L'altro elenco ha prodotto un codice non ottimizzato. È sicuro passare in un debugger, perché si assicura che sia possibile posizionare un punto di interruzione ovunque e ispezionare lo stato di qualsiasi variabile in qualsiasi riga del programma. È più lento e più difficile da leggere. La versione ottimizzata è molto più vicina al codice che verrà effettivamente eseguito in produzione.
Il parametro a
di questa funzione viene passato in una coppia di registri a 64 bit, rsi: rdi. Il risultato viene restituito in un'altra coppia di registri, rdx: rax. Le prime due righe di codice inizializzano la somma in a
.
La terza riga aggiunge 1337 alla parola bassa dell'input. Se questo trabocca, porta l'1 nel flag carry della CPU. La quarta riga aggiunge zero alla parola più alta dell'input, più 1 se viene trasportata.
Puoi pensare a questo come una semplice aggiunta di un numero di una cifra a un numero di due cifre
a b
+ 0 7
______
ma in base 18.446.744.073.709.551.616. Stai ancora aggiungendo prima la "cifra" più bassa, possibilmente portando un 1 nella colonna successiva, quindi aggiungendo la cifra successiva più il carry. La sottrazione è molto simile.
La moltiplicazione deve utilizzare l'identità (2⁶⁴a + b) (2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴ (ad + bc) + bd, dove ciascuna di queste moltiplicazioni restituisce la metà superiore del prodotto in un registro e la metà inferiore del prodotto in un altro. Alcuni di questi termini verranno eliminati, poiché i bit sopra il 128 ° non rientrano in a u128
e vengono scartati. Anche così, questo richiede una serie di istruzioni della macchina. Anche la divisione compie diversi passaggi. Per un valore con segno, la moltiplicazione e la divisione dovrebbero inoltre convertire i segni degli operandi e il risultato. Tali operazioni non sono affatto molto efficienti.
Su altre architetture, diventa più facile o più difficile. RISC-V definisce un'estensione del set di istruzioni a 128 bit, sebbene per quanto ne sappia nessuno lo ha implementato in silicio. Senza questa estensione, il manuale di architettura RISC-V raccomanda un ramo condizionale:addi t0, t1, +imm; blt t0, t1, overflow
SPARC ha codici di controllo come i flag di controllo di x86, ma è necessario utilizzare un'istruzione speciale add,cc
per impostarli. MIPS, d'altra parte, richiede di verificare se la somma di due numeri interi senza segno è strettamente inferiore a uno degli operandi. In tal caso, l'aggiunta è traboccata. Almeno sei in grado di impostare un altro registro sul valore del bit di riporto senza un ramo condizionale.