Как уже отмечали другие, вся 64-битная арифметика в вашем примере была оптимизирована. Этот ответ фокусируется на вопросе в заголовке.
По сути, мы рассматриваем каждое 32-разрядное число как цифру и работаем в базе 4294967296. Таким образом, мы можем работать с произвольно большими числами.
Сложение и вычитание проще всего. Мы работаем через цифры по одному, начиная с наименее значимого и переходя к наиболее значимому. Обычно первая цифра выполняется с помощью обычной инструкции сложения / вычитания, а последующие цифры - с использованием специальной инструкции «сложить с переносом» или «вычесть с заимствованием». Флаг переноса в регистре состояния используется для переноса бита переноса / заимствования из одной цифры в другую. Благодаря двойному дополнению подписи и без знака сложения и вычитания одинаковы.
Умножение немного сложнее, умножение двух 32-разрядных цифр может дать 64-разрядный результат. Большинство 32-битных процессоров будут иметь инструкции, которые умножают два 32-битных числа и выдают 64-битный результат в двух регистрах. Дополнение будет необходимо, чтобы объединить результаты в окончательный ответ. Благодаря двойному дополнению подпись и умножение без знака одинаковы при условии, что желаемый размер результата совпадает с размером аргумента. Если результат больше аргументов, требуется особая осторожность.
Для сравнения начнем с самой значимой цифры. Если оно равно, мы переходим к следующей цифре, пока результаты не будут равны.
Деление слишком сложно для меня, чтобы описать в этом посте, но есть множество примеров алгоритмов. например http://www.hackersdelight.org/hdcodetxt/divDouble.c.txt
Некоторые примеры из gcc https://godbolt.org/g/NclqXC, ассемблер в синтаксисе intel.
Первое дополнение. добавление двух 64-битных чисел и получение 64-битного результата. Asm одинакова для подписанной и неподписанной версий.
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
Это довольно просто: загрузить один аргумент в eax и edx, затем добавить другой, используя add, а затем add с переносом. Результат сохраняется в eax и edx для возврата вызывающей стороне.
Теперь умножение двух 64-битных чисел для получения 64-битного результата. Снова код не меняется с подписанного на неподписанный. Я добавил несколько комментариев, чтобы было легче следить.
Прежде чем мы рассмотрим код, давайте рассмотрим математику. a и b - 64-разрядные числа. Я буду использовать lo () для представления младших 32-разрядных чисел 64-разрядного числа и hi () для представления старших 32-разрядных чисел 64-разрядного числа.
(a * b) = (lo (a) * lo (b)) + (hi (a) * lo (b) * 2 ^ 32) + (hi (b) * lo (a) * 2 ^ 32) + (привет (б) * привет (а) * 2 ^ 64)
(a * b) mod 2 ^ 64 = (lo (a) * lo (b)) + (lo (hi (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 (а))
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
Наконец, когда мы пробуем деление, мы видим.
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
Компилятор решил, что деление слишком сложно для реализации inline, и вместо этого вызывает библиотечную процедуру.