Aby zapewnić być może jaśniejszy przykład, na platformie x86_64, skompilowanej z -O
flagą, funkcja
pub fn leet(a : i128) -> i128 {
a + 1337
}
kompiluje się do
example::leet:
mov rdx, rsi
mov rax, rdi
add rax, 1337
adc rdx, 0
ret
(Mój oryginalny post miał u128
raczej niż ten i128
, o który pytałeś. Funkcja kompiluje ten sam kod w obu przypadkach, dobra demonstracja, że dodawanie ze znakiem i bez znaku jest takie samo na nowoczesnym procesorze.)
Druga lista wygenerowała niezoptymalizowany kod. Bezpiecznie jest przejść przez debuger, ponieważ zapewnia on możliwość umieszczenia punktu przerwania w dowolnym miejscu i sprawdzenia stanu dowolnej zmiennej w dowolnym wierszu programu. Czytanie jest wolniejsze i trudniejsze. Zoptymalizowana wersja jest znacznie bliższa kodowi, który faktycznie będzie działał w środowisku produkcyjnym.
Parametr a
tej funkcji jest przekazywany w parze rejestrów 64-bitowych, rsi: rdi. Wynik jest zwracany w innej parze rejestrów, rdx: rax. Pierwsze dwa wiersze kodu inicjują sumę do a
.
Trzecia linia dodaje 1337 do młodszego słowa wejścia. Jeśli to się przepełni, przenosi 1 we fladze przenoszenia procesora. Czwarta linia dodaje zero do starszego słowa wejścia - plus 1, jeśli zostało przeniesione.
Możesz o tym myśleć jako o prostym dodaniu liczby jednocyfrowej do liczby dwucyfrowej
a b
+ 0 7
______
ale w bazie 18,446,744,073,709,551,616. Nadal dodajesz najpierw najniższą „cyfrę”, prawdopodobnie przenosząc 1 do następnej kolumny, a następnie dodajesz następną cyfrę plus przeniesienie. Odejmowanie jest bardzo podobne.
Mnożenie musi wykorzystywać tożsamość (2⁶⁴a + b) (2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴ (ad + bc) + bd, gdzie każde z tych mnożeń zwraca górną połowę iloczynu w jednym rejestrze i dolną połowę iloczynu w inne. Niektóre z tych terminów zostaną usunięte, ponieważ bity powyżej 128-go nie pasują do a u128
i są odrzucane. Mimo to wymaga to wielu instrukcji maszynowych. Podział ma również kilka kroków. W przypadku wartości ze znakiem mnożenie i dzielenie musiałyby dodatkowo konwertować znaki operandów i wynik. Te operacje wcale nie są zbyt wydajne.
Na innych architekturach staje się to łatwiejsze lub trudniejsze. RISC-V definiuje 128-bitowe rozszerzenie zestawu instrukcji, chociaż według mojej wiedzy nikt nie zaimplementował go w krzemie. Bez tego rozszerzenia podręcznik architektury RISC-V zaleca gałąź warunkową:addi t0, t1, +imm; blt t0, t1, overflow
SPARC ma kody sterujące, takie jak flagi kontrolne x86, ale musisz użyć specjalnej instrukcji add,cc
, aby je ustawić. Z drugiej strony MIPS wymaga sprawdzenia, czy suma dwóch liczb całkowitych bez znaku jest dokładnie mniejsza niż jeden z operandów. Jeśli tak, dodatek się przepełnił. Przynajmniej możesz ustawić inny rejestr na wartość przenoszonego bitu bez gałęzi warunkowej.