Стоит задача деления двух чисел, то есть нахождения частного от деления и остатка. При этом предполагается использовать встроенную в некоторый w-битный процессор инструкцию деления двухразрядного числа ( то есть 2w битного) на одноразрядное (w-битное), которая даёт лишь одноразрядные частное и остаток.

Ограничимся делением двухразрядных чисел без знака. Деление чисел большей разрядности можно обобщить, при необходимости обратившись к первоисточнику [1, п.4.3.1]. Описываемый алгоритм назовем «программный 128/128». Заметим, что во многих 64-битных компиляторах он уже реализован (GCC, Clang, Intel Compiler) и может быть использован напрямую без изобретения велосипеда.

Цель данной статьи — подробно объяснить детали алгоритма, чтобы снизить порог входа в общеизвестные труды Д. Кнута, в том числе объяснить почему деление в процессоре дает лишь одноразрядное частное (конкретно для 64-битных процессоров можно делить 128-битное число на 64-битное, получая лишь 64-битные частное и остаток). Назовем процессорный алгоритм деления как «аппаратный 128/64».

Ключевым моментом в понимании алгоритмов деления является процесс нормализации чисел, который и позволяет смело пользоваться встроенным в процессор делением 128/64, не опасаясь непредсказуемого поведения процессора.

Алгоритм деления двухразрядных чисел можно классифицировать по фактической разрядности делителя на два алгоритма:

  1. Половинчатое программное деление, когда делитель одноразрядный,

  2. Полное программное деление, когда делитель двухразрядный.

Заметим, что аппаратный алгоритм 128/64 является половинчатым; он будет использован в обеих ветках программного алгоритма. Несмотря на привязку к 64-битным процессорам, в описываемых алгоритмах будем использовать обозначения для w-битного процессора.

Половинчатое программное деление

Задача - разделить два числа

[q, r] = x / y.

Здесь делитель y занимает один разряд (пусть величина разряда равна b, а битовая ширина равна w), а делимое x - два разряда. Перед делением делаем нормализацию делимого и делителя, которая не меняет частного q; остаток при этом придется масштабировать обратно, чтобы получить оригинальный остаток r. Процесс нормализации заключается в одновременном умножении двух чисел (делимого и делителя) на некоторое число до тех пор, пока делитель не станет больше (или равным) половине разряда:

y' \ge \lfloor {b/2} \rfloor.

На двоичных компьютерах нормализацию выгодно делать битовым сдвигом влево (то есть умножением на два). Также удобно считать, что величина разряда делится на два. Нормализованное число x' будет в общем случае трехразрядным, потому что делитель будет сдвинут максимум на w-1 бит, чтобы получить единицу в старшем бите, и, соответственно, двухразрядное делимое будет сдвинуто максимум на w-1 бит, что не позволит ему выйти за рамки трехразрядного числа. Признаком нормальности делителя является старший бит, установленный в единицу. Этого всегда можно достичь, кроме случая y = 0, что должно быть исключено заранее.

При делении трехразрядного числа на одноразрядное в столбик представляется естественным сначала разделить два старших разряда. Покажем, что частное от деления двух старших разрядов делимого на нормализованный делитель будет всегда одноразрядным числом. До нормализации делимое могло быть не более(b-1)b + (b-1), а делитель не мог быть менее единицы. Нормализация приведет к умножению максимум на b/2 (когда делитель равен единице), поэтому трехразрядное число будет вида b^2\lfloor(b-1)/2\rfloor + b(b-1) + b/2, а делитель равен b/2. Рассмотрение двух старших разрядов делимого как некоторого числа позволяет записать его как b\lfloor(b-1)/2\rfloor + (b-1), поэтому частное от деления такого числа на делитель b/2 будет ограничено:

{{b\lfloor(b-1)/2\rfloor + (b-1)} \over {b/2}} = {{b(b-2) + 2(b-1)} \over {b}} = {{b^2-2} \over {b}} < b.

Это и позволяет автоматически воспользоваться аппаратным алгоритмом "128/64", что упрощает (и ускоряет) программный алгоритм деления.

Эй, школяр! Нормализуй при делении в столбик!

Заметим, что техникой нормализации можно пользоваться при школьном делении в столбик десятичных чисел. Это позволит в некоторой степени автоматизировать процедуру. При этом нормализацию можно делать также умножением на два.

Получившийся остаток, который в данном граничном случае равен\lfloor(b-1)/2\rfloor (смотри пояснение ниже*), если к нему приставить младший разряд делимого, будет двухразрядным числом, которое также можно разделить на делитель аппаратным алгоритмом "128/64":

{{b\lfloor(b-1)/2\rfloor + b/2} \over {b/2}} = {{b(b-2)/2 + b/2} \over {b/2}} = b-1 < b.

*Величину остатка можно получить, принимая частное за b-1, и прямо вычитая результат умножения (частного на делитель) из делимого:

[b(b-2) + 2(b-1)] - b \cdot (b-1) = b - 2,

при этом получившийся здесь остаток следует разделить на два, потому что мы взяли делимое и делитель из середины формулы, где они умножены на два:

{{b-2} \over {2}} = {\lfloor{(b-1)/2}\rfloor}.
Разбери сам!

Описанный выше граничный случай деления можно проиллюстрировать на примере деления чисел, разряд которых равен, например, 256. В этом случае разряды будут наглядно отображены (например, в калькуляторе) в виде 8-битных слов.

Два получившихся частных следует объединить в двухразрядное число, которое даст итоговое частное от деления, а остаток от последнего (аппаратного) деления следует нормализовать в обратную сторону, то есть привести к исходному масштабу (делением на два Q раз, если нормализация заключалась в умножении на два Q раз).

Отметим, что половинчатое деление (помимо нормализации) в итоге требует два аппаратных деления.

Алгоритм половинчатого деления:

  1. Умножением на два нормализовать делимое x и делитель y, так, чтобы старший бит делителя был равен единице. После нормализации делимое обозначается как x', а делитель - как y'.

  2. Разделить двухразрядное делимое, образованное из старших разрядов трехразрядного делимого x', на одноразрядный делитель, используя аппаратный алгоритм деления.

  3. Скомпоновать из найденного в пункте 2 остатка и младшего разряда делимого x' новое двухразрядное делимое.

  4. Разделить новое двухразрядное делимое на одноразрядный делитель, используя аппаратный алгоритм деления.

  5. Из найденных на этапах 2 и 4 частных скомпоновать двухразрядное частное, которое будет итоговым частным.

  6. Найденный на этапе 4 остаток разделить на два столько раз, сколько раз было умножение на два в процедуре нормализации. Получившийся остаток будет итоговым остатком.

Полное программное деление

Здесь делимое и делитель занимают два разряда. Выполняем процедуру нормализации, после которой делитель будет двухразрядным числом со старшим битом, установленным в единицу, а делимое - некоторым в общем случае трехразрядным числом. На данном этапе уже можно применить аппаратное деление, организуя делимое из двух старших разрядов делимого, а делитель - из старшего разряда делителя. Это следует из подробного анализа половинчатого программного алгоритма деления, потому что нормализация одинаковым образом затрагивает старшие разряды делимого и делителя безотносительно к их младшим разрядам. В результате аппаратного деления получается одноразрядное частное и одноразрядный остаток. Данный остаток следует скомпоновать с младшим разрядом делимого и получить таким образом некоторый двухразрядный остаток (для лучшего понимания повествования можно нарисовать процедуру деления в столбик некоторого трехразрядного числа на некоторое двухразрядное).

После аппаратного деления следует сделать коррекцию найденных частного и остатка, по той причине, что не был учтён младший разряд делителя. Для этого найденное одноразрядное частное необходимо умножить на младший разряд делителя, формируя тем самым некоторое двухразрядное число. Если это число меньше (или равно) текущего двухразрядного остатка, то его следует вычесть из остатка и получить остаток, который после процедуры, обратной к процедуре нормализации, будет итоговым остатком. Если же число окажется больше, то у частного следует сделать декремент, а к результату вычитания (который будет отрицательным по смыслу), соответственно, прибавить исходный нормализованный делитель. Скорректированный таким образом остаток также необходимо пропустить через процедуру, обратную нормализации.

Отметим, что полное программное деление (помимо нормализации) требует одно аппаратное деление и одно расширяющее умножение с одним-двумя вычитаниями.

Алгоритм полного деления:

  1. Умножением на два нормализовать делимое x и делитель y, так, чтобы старший бит делителя был равен единице. После нормализации делимое обозначается как x', а делитель - как y'.

  2. Разделить двухразрядное делимое, образованное из двух старших разрядов трехразрядного делимого x', на старший разряд делителя, используя аппаратный алгоритм деления.

  3. Скомпоновать из найденного в пункте 2 остатка и младшего разряда делимого x' двухразрядный остаток R.

  4. Умножить младший разряд делителя y' на найденное в пункте 2 частное q, формируя некоторое двухразрядное число T.

  5. Если T \le R, то сформировать остаток r' = R - T. Перейти к пункту 7.

  6. Если T > R, то сформировать остаток r' = y' + R - T и декрементировать частноеq = q - 1.

  7. Найденный на этапах 5 или 6 остаток r' разделить на два столько раз, сколько раз было умножение на два в процедуре нормализации. Получившийся остаток будет итоговым остатком. Найденное частное q будет итоговым частным.

Кое что про обобщение деления на многоразрядные числа

Например, требуется разделить два трехразрядных числа (делитель имеет ненулевой третий разряд). После процедуры нормализации выходит так, что следует разделить четырехразрядное число на трехразрядное. Мы можем взять три старших разряда делимого и разделить на два старших разряда делителя, используя описанное выше полное программное деление (правда, без этапа нормализации, так как он уже сделан). И далее применить коррекцию неучтенного младшего разряда делителя аналогично пунктам 4-6 алгоритма полного программного деления. И так далее рекурсивно... В итоге, базовым при реализации арифметики произвольной точности будет алгоритм деления n+1 разрядного числа на нормализованное n разрядное, что согласуется с [1].

Вывод

Половинчатое программное деление требует два последовательных аппаратных деления. Полное же программное деление требует одно аппаратное деление с последующим расширяющим умножением и дополнительными вычитаниями (одно или два). Дополнительно, оба алгоритма требуют нормализации делимого и делителя, а также денормализации остатка, что требует операций левого и правого битового сдвигов.

Ключевым моментом в алгоритмах деления является процедура нормализации, которая позволяет использовать аппаратные возможности процессора и упрощает алгоритм программного деления.

Источники

  1. Дональд Э. Кнут. Искусство программирования, Т.2., Получисленные алгоритмы, 3-е издание, 2018.

Комментарии (0)