Постановка задачи

Рассматривается целое (беззнаковое) число шириной N бит. Требуется вычислить частное и остаток при делении

[q,~r] = {{2^N} \over {D}},

имея в распоряжении N-битную арифметику.

Данная величина - назовем ее extended reciprocal (ER) - может пригодиться, например, при организации алгоритма деления "широкого" числа на "узкое"

{{A \cdot {2^N} + B} \over {D}}.

Здесь под широким мы понимаем число разрядности 2N, а под узким - число разрядности N бит.

Решение задачи

Так как число 2^Nне входит в диапазон целых N-битных чисел, то напрямую вычислить ER не получится. Очевидно, что при D = 0 величина не определена, а при D = 1 равна числу 2^N, что эквивалентно нулю и может быть обработано отдельно. Остается решить задачу для D > 1.

Расширим число D максимально возможно, умножая его на два, что соответствует битовому сдвигу числа в сторону старшего разряда "до упора", когда старшая единица займет крайний разряд N-битного числа:

D' = D \cdot 2^i, ~~~D' < 2^N, ~i \geq 0.

Здесь индекс i определяет то, на сколько битов удалось сдвинуть число D, чтобы получить расширенное число D' - максимально большое и при этом сохраняющее всю информацию об исходном числе D. Величина 2^i будет наибольшим приближением к частному от деления q, если бы была доступна только операция расширения:

q_0 = 2^i, ~~r_0 = 0.

Далее необходимо из 2^N вычесть D', что позволит вычислить добавочную величину к частному и, за одно, остаток от деления r. Так как в беззнаковой арифметике 2^N \equiv 0, то, фактически, потребуется взять знак "минус":

\Delta = 2^N - D' = -D' \mod 2^N.

Данная "дельта" - это то, что скорректирует частное и даст искомый остаток:

[q_1,~r_1] = {{\Delta} \over {D}} = {{-D'} \over {D}}.

Окончательно имеем

q = q_0 + q_1, ~~r = r_0 + r_1 = r_1.

Заметим, что данный алгоритм актуален при работе с беззнаковыми числами фиксированной разрядности N бит, поэтому знак минус работает так, как если принять аксиому 2^N \equiv 0, при этом рассматривать знак отдельно (в уме) нельзя - он становится неотъемлемой частью своего числа.

Расширение беззнакового числа может быть сделано, например, функцией библиотеки STL

auto i = std::countl_zero(D);

В итоге, получается такая функция

template <std::unsigned_integral T>
inline std::pair<T, T> reciprocal_and_extend(T x)
{
    assert(x != 0);
    const auto x_old = x;
    const auto i = std::countl_zero(x);
    x <<= i;
    const T &r = (-x) % x_old;
    const T &q = (-x) / x_old + (T{1} << i);
    return {q, r};
}

Алгоритм деления широкого числа на узкое

Пусть стоит задача нахождения частного и остатка от деления широкого числа на узкое:

[Q,~R]= {{A \cdot {2^N} + B} \over {D}}.

Здесь N- битовая ширина узкого числа (или полуширина широкого числа). Данное деление может пригодиться как самостоятельно, так и, например, как часть алгоритма деления широких чисел

{W_1 \over W_2} = {{A \cdot {2^N} + B} \over { C \cdot {2^N} + D}}.

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

[q,~r] = {{2^N-1} \over {D}},

которая вычисляется встроенным делением. Для простоты остатком r пренебрегали. Тот алгоритм требует небольшого количества итераций (мода распределения ~N/4). Однако, возможны случаи, когда остаток r близок к D, и для больших D это приводит к росту количества итераций вплоть до N.

В рассматриваемом алгоритме будем вычислять дробь

[q,~r]= {{2^N} \over {D}}

напрямую, используя основной алгоритм текущей статьи как составную часть. Также не будем пренебрегать остатком от деления. Однако, такой подход всё равно приведет к случаям роста количества итераций вплоть до N из-за существования остатков r, близких к D. Это так, потому что

[Q,~R]= {{A \cdot {2^N} + B} \over {D}} = A \cdot {{2^N}\over{D}} + {{B}\over{D}} = A \cdot q + {{B}\over{D}} + {{A \cdot r}\over{D}}.

Здесь

{{A \cdot r}\over{D}} = {{A' \cdot {2^N} + B'}\over{D}},

то есть задача сводится к новому делению (следующей итерации), и при r \approx D количество итераций возрастет, так как в этом случае деление выгоднее было бы вычислять, используя дополнение:

 {{A \cdot (D - r)} \over {D}} = {A - {{A \cdot r} \over {D}}},

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

D - r < r.

В этом случае удобно увеличить q на единицу, потому что

[Q,~R] = A \cdot q + {{B}\over{D}} + {{A \cdot r}\over{D}} = A \cdot (q + 1) + {{B}\over{D}} - {{A \cdot (D - r)}\over{D}},

тогда каждая новая итерация потребует всего лишь смены знака - как у частного, так и, соответственно, у остатка. В итоге, как частное, так и остаток в процессе итераций будут накапливаться с чередованием знака. В конце итераций, когда числитель будет равен нулю, потребуется сделать тонкую коррекцию знака накопленных частного и остатка, если частное окажется больше делимого (в арифметике беззнаковых чисел). Тонкую - потому что смена знака у остатка имеет особенность из-за того, что остаток "работает" по модулю D:

-(R) = D - R,

при этом если, например, R = 0, то при смене знака остаток станет равным D, и потребуется обнулить остаток и инкрементировать частное. Дополнительно, накопление остатка должно сопровождаться отслеживанием переполнения: это объясняется тем, что остаток работает по модулю D, но из-за арифметики ограниченной разрядности одновременно происходит работа и по модулю 2^N: при переполнении как раз пригождается заранее вычисленный остаток от деления

r = {{2^N} \mod {D}}.

Это так, потому что если при увеличении остатка на некую величину \delta < D происходит переполнение, то имеем:

R + \delta = R + \delta - 2^N = R'.

Здесь

R' = R + \delta - 2^N

- величина, получаемая при сложении R и \delta в беззнаковой арифметике по модулю 2^N для случая переполнения. Следовательно, по модулю D имеем:

R + \delta \equiv [R' + 2^N \mod D] \mod D.

В итоге, получается следующий алгоритм увеличения остатка.

template <std::unsigned_integral T>
inline T smart_remainder_adder(T &remainder, T delta, const T &modulo)
{
    assert(modulo != 0);
    assert(remainder < modulo);
    const auto rem_old = remainder;
    delta %= modulo;    
    const T &summ = rem_old + delta;
    remainder = summ;
    T carry = summ >= modulo ? T{1} : T{0};
    if (auto overflow = summ < std::min(rem_old, delta); overflow)
    {
        const auto& [_, r_rec] = reciprocal_and_extend(modulo);
        remainder += r_rec;
        carry = T{1};
    }   
    remainder %= modulo;
    return carry;
}

Пара для следующей итерации

(A',~B')

требует умножения двух N-битных чисел с расширением до 2N, которое реализовано, например, здесь на примере арифметики 128-битных чисел совместно с обсуждаемыми здесь алгоритмами деления.

В целом, алгоритм деления для некоторого класса "широких" чисел U и "узких" чисел типа T выглядит так
template <class U, std::unsigned_integral T>
inline std::pair<U, T> div(U X, const T &Y)
{
    assert(Y != 0);
    if (Y == T{1})
        return {X, 0};    
    U Q{0};
    T R = 0;
    auto rcp = reciprocal_and_extend(Y);
    const auto &rcp_compl = Y - rcp.second;
    const bool make_inverse = rcp_compl < rcp.second; // Для ускорения сходимости.
    rcp.first += make_inverse ? T{1} : T{0};
    const auto X_old = X;
    for (;;)
    {
        const bool x_has_high = X.high() != 0;
        Q += x_has_high ? mult64(X.high(), rcp.first) : 0ull;
        Q += U{X.low() / Y};
        const auto& carry = smart_remainder_adder(R, X.low(), Y, rcp.second);
        Q += carry;
        X = X.high() != 0ull ? mult64(X.high(), make_inverse ? rcp_compl : rcp.second) : 0ull;
        if (X == U{0})
        {
            if (Q > X_old) // Коррекция знака.
            {
                Q = -Q;
                R = Y - R; // mod Y
                if (R == Y)
                {
                    Q.inc();
                    R = 0;
                }
            }
            break;
        }
        if (make_inverse)
        {
            Q = -Q;
            R = Y - R; // mod Y
        }
    }
    return {Q, R};
}

Использование трюка с чередованием знака (на примере 128-битных чисел) снижает максимальное значение количества итераций с 64 до примерно 42 (вероятно, до ~2N/3, N = 64), при этом среднее значение (мат. ожидание) снижается примерно с 27 до 21. Мода распределения практически не изменяется.

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