Можно ли эффективно вычислить A% B без необходимости вычисления A / B? - PullRequest
2 голосов
/ 08 апреля 2019

Я пишу библиотеку с высокой точностью на C ++, используя базу 2 ^ 64, и в настоящее время я работаю над операцией mod. Я использую Алгоритм D, описанный в издании "Искусство компьютерного программирования" Тома Дональда Кнута, 1998 год. 2, раздел 4.3.1, для деления, которое дает частное и остаток. Для операции mod я выполняю деление, выбрасывая в конце частное. Хотя Алгоритм D Кнута очень быстр, если он реализован в C ++ с некоторыми улучшениями ASM для частичного деления и одновременного умножения / вычитания с множественной точностью на каждом шаге, я не уверен, что есть лучший способ, так как выбрасывание тщательно вычисляется результат не кажется мне эффективным.

К сожалению, в Алгоритме D невозможно избавиться от частичного деления, потому что частное отношение требуется для вычисления остатка, путем итеративного вычитания произведения частного фактора и делителя из дивиденда.

Я искал в Интернете альтернативные решения и нашел влиятельные статьи, написанные Полом Барреттом и Питером Л. Монтгомери . Тем не менее, используемые ими причудливые уловки окупаются только в том случае, если много операций mod выполняются подряд с одним и тем же модулем, поскольку они предполагают тяжелые предварительные вычисления. Это имеет место в сложных операциях, таких как модульное возведение в степень, где mod из нескольких квадратов и произведений требуется для одного модуля. Барретт начинается с базового определения остатка, r = a - b * (a / b), и изменяет деление на умножение с обратной величиной b. Затем он представляет эффективный способ вычисления этого умножения, который окупается, если обратное вычисляется один раз для нескольких аналогичных вычислений. Монтгомери превращает операнды в совершенно другую систему вычетов, где модульная арифметика дешева, но за цену преобразований туда-сюда.

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

Итак, что мне нужно, так это эффективная однократная mod функция без накладных расходов и особых ограничений. Отсюда мой вопрос: возможно ли вычислить остаток без вычисления фактора во-первых, более эффективным способом, чем деление?

1 Ответ

0 голосов
/ 08 апреля 2019

Одним из предложений было бы написать простую функцию, которая будет вычислять A%B=C и сохранять значения A, B и C в массиве, а затем сохранять все результаты в векторе.Затем распечатайте их, чтобы увидеть отношения всех входных и выходных значений.

Есть одна вещь, которую можно сделать, чтобы упростить некоторые из этой работы, а именно узнать некоторые свойства функции мода.Эти два утверждения помогут вам с функцией.

 0 mod N = 0
 N mod 0 = undefined

Начиная с 0 mod N = 0 мы можем поставить тестовый случай для A, а если это 0, мы можем простоиспользуйте это, чтобы заполнить наш массив.Аналогично, если B = 0, мы можем заполнить значение C нашего массива -1 просто для представления неопределенного, потому что вы не можете выполнить A mod 0, так как компиляция не удастся из-за деления на 0.

Я написал эту функцию, чтобы сделать это;затем я запускаю его через цикл для A & B из [0,15].

#include <array>
#include <vector>
#include <iostream>

std::array<int, 3> calculateMod(int A, int B) {
    std::array<int, 3 > res;
    if (A == 0) {       
        res = std::array<int, 3>{ 0, B, 0 };
    }
    else if (B == 0) {
        res = std::array<int, 3>{ A, 0, -1 };
    }
    else {
        res = std::array<int, 3>{ A, B, A%B };
    }
    return res;
}

int main() {
    std::vector<std::array<int, 3>> results;

    int N = 15; 
    for (int A = 0; A <= N; A++) {
        for (int B = 0; B <= N; B++) {
            results.push_back(calculateMod(A, B));
        }
    }

    // Now print out the results in a table form:
    int i = 0; // Index for formatting output
    for (auto& res : results) {
        std::cout << res[0] << " % " << res[1] << " = " << res[2] << '\n';

        // just for formatting output data to make it easier to read.
        i++;
        if ( i > N ) {
            std::cout << '\n';
            i = 0;
        }
    }
    return 0;
}

Вот его вывод:

0 % 0 = 0
0 % 1 = 0
0 % 2 = 0
0 % 3 = 0
0 % 4 = 0
0 % 5 = 0
0 % 6 = 0
0 % 7 = 0
0 % 8 = 0
0 % 9 = 0
0 % 10 = 0
0 % 11 = 0
0 % 12 = 0
0 % 13 = 0
0 % 14 = 0
0 % 15 = 0

1 % 0 = -1
1 % 1 = 0
1 % 2 = 1
1 % 3 = 1
1 % 4 = 1
1 % 5 = 1
1 % 6 = 1
1 % 7 = 1
1 % 8 = 1
1 % 9 = 1
1 % 10 = 1
1 % 11 = 1
1 % 12 = 1
1 % 13 = 1
1 % 14 = 1
1 % 15 = 1

2 % 0 = -1
2 % 1 = 0
2 % 2 = 0
2 % 3 = 2
2 % 4 = 2
2 % 5 = 2
2 % 6 = 2
2 % 7 = 2
2 % 8 = 2
2 % 9 = 2
2 % 10 = 2
2 % 11 = 2
2 % 12 = 2
2 % 13 = 2
2 % 14 = 2
2 % 15 = 2

3 % 0 = -1
3 % 1 = 0
3 % 2 = 1
3 % 3 = 0
3 % 4 = 3
3 % 5 = 3
3 % 6 = 3
3 % 7 = 3
3 % 8 = 3
3 % 9 = 3
3 % 10 = 3
3 % 11 = 3
3 % 12 = 3
3 % 13 = 3
3 % 14 = 3
3 % 15 = 3

4 % 0 = -1
4 % 1 = 0
4 % 2 = 0
4 % 3 = 1
4 % 4 = 0
4 % 5 = 4
4 % 6 = 4
4 % 7 = 4
4 % 8 = 4
4 % 9 = 4
4 % 10 = 4
4 % 11 = 4
4 % 12 = 4
4 % 13 = 4
4 % 14 = 4
4 % 15 = 4

5 % 0 = -1
5 % 1 = 0
5 % 2 = 1
5 % 3 = 2
5 % 4 = 1
5 % 5 = 0
5 % 6 = 5
5 % 7 = 5
5 % 8 = 5
5 % 9 = 5
5 % 10 = 5
5 % 11 = 5
5 % 12 = 5
5 % 13 = 5
5 % 14 = 5
5 % 15 = 5

6 % 0 = -1
6 % 1 = 0
6 % 2 = 0
6 % 3 = 0
6 % 4 = 2
6 % 5 = 1
6 % 6 = 0
6 % 7 = 6
6 % 8 = 6
6 % 9 = 6
6 % 10 = 6
6 % 11 = 6
6 % 12 = 6
6 % 13 = 6
6 % 14 = 6
6 % 15 = 6

7 % 0 = -1
7 % 1 = 0
7 % 2 = 1
7 % 3 = 1
7 % 4 = 3
7 % 5 = 2
7 % 6 = 1
7 % 7 = 0
7 % 8 = 7
7 % 9 = 7
7 % 10 = 7
7 % 11 = 7
7 % 12 = 7
7 % 13 = 7
7 % 14 = 7
7 % 15 = 7

8 % 0 = -1
8 % 1 = 0
8 % 2 = 0
8 % 3 = 2
8 % 4 = 0
8 % 5 = 3
8 % 6 = 2
8 % 7 = 1
8 % 8 = 0
8 % 9 = 8
8 % 10 = 8
8 % 11 = 8
8 % 12 = 8
8 % 13 = 8
8 % 14 = 8
8 % 15 = 8

9 % 0 = -1
9 % 1 = 0
9 % 2 = 1
9 % 3 = 0
9 % 4 = 1
9 % 5 = 4
9 % 6 = 3
9 % 7 = 2
9 % 8 = 1
9 % 9 = 0
9 % 10 = 9
9 % 11 = 9
9 % 12 = 9
9 % 13 = 9
9 % 14 = 9
9 % 15 = 9

10 % 0 = -1
10 % 1 = 0
10 % 2 = 0
10 % 3 = 1
10 % 4 = 2
10 % 5 = 0
10 % 6 = 4
10 % 7 = 3
10 % 8 = 2
10 % 9 = 1
10 % 10 = 0
10 % 11 = 10
10 % 12 = 10
10 % 13 = 10
10 % 14 = 10
10 % 15 = 10

11 % 0 = -1
11 % 1 = 0
11 % 2 = 1
11 % 3 = 2
11 % 4 = 3
11 % 5 = 1
11 % 6 = 5
11 % 7 = 4
11 % 8 = 3
11 % 9 = 2
11 % 10 = 1
11 % 11 = 0
11 % 12 = 11
11 % 13 = 11
11 % 14 = 11
11 % 15 = 11

12 % 0 = -1
12 % 1 = 0
12 % 2 = 0
12 % 3 = 0
12 % 4 = 0
12 % 5 = 2
12 % 6 = 0
12 % 7 = 5
12 % 8 = 4
12 % 9 = 3
12 % 10 = 2
12 % 11 = 1
12 % 12 = 0
12 % 13 = 12
12 % 14 = 12
12 % 15 = 12

13 % 0 = -1
13 % 1 = 0
13 % 2 = 1
13 % 3 = 1
13 % 4 = 1
13 % 5 = 3
13 % 6 = 1
13 % 7 = 6
13 % 8 = 5
13 % 9 = 4
13 % 10 = 3
13 % 11 = 2
13 % 12 = 1
13 % 13 = 0
13 % 14 = 13
13 % 15 = 13

14 % 0 = -1
14 % 1 = 0
14 % 2 = 0
14 % 3 = 2
14 % 4 = 2
14 % 5 = 4
14 % 6 = 2
14 % 7 = 0
14 % 8 = 6
14 % 9 = 5
14 % 10 = 4
14 % 11 = 3
14 % 12 = 2
14 % 13 = 1
14 % 14 = 0
14 % 15 = 14

15 % 0 = -1
15 % 1 = 0
15 % 2 = 1
15 % 3 = 0
15 % 4 = 3
15 % 5 = 0
15 % 6 = 3
15 % 7 = 1
15 % 8 = 7
15 % 9 = 6
15 % 10 = 5
15 % 11 = 4
15 % 12 = 3
15 % 13 = 2
15 % 14 = 1
15 % 15 = 0

Из приведенных выше данных мы можемвидите, что если A == B, то результат будет 0.Мы также видим, что если B > A, то B == A.Наконец, мы можем видеть, что существуют шаблоны между значениями odd и even, равными A, тогда как B < A.Если вы можете понять эти паттерны, то большая их часть становится алгебраической манипуляцией.Отсюда следующим шагом будет создание алгоритма, который будет принимать все эти данные и преобразовывать их в двоичную эквивалентность.

Я выбрал значение N выше как 15 по причине.Это связано с двоичным представлением всех возможных комбинаций двоичных цифр, прежде чем они повторяются снова.Мы знаем, что один байт данных составляет 8 бит;мы знаем, что значения из [0,15] будут вписываться в половину этого;например:

binary byte:  hex    decimal
0000 0000     0x00   0
...
0000 1111     0xFF   15

После этих 15 различных последовательностей 0 и 1 эти паттерны будут повторяться.Итак, взяв таблицу выше, вы можете преобразовать их в двоичные представления.Теперь, как только вы изучите представления A & B входов с их C выходами в двоичном виде и поймете 3 свойства результатов, которые я упомянул выше;Вы должны быть в состоянии разработать алгоритм для быстрого вычисления по модулю любой комбинации A B.Один трюк, который нужно помнить, - это то, что нужно учитывать еще 3 вещи.Первое, что сказал пользователь eerokia:

«В частности, по модулю со степенью 2 можно заменить побитовые операции».

Следующеекроме того, значения являются четными или нечетными, так как четные и нечетные случаи представляют различные варианты A mod B, когда B < A.

У меня есть некоторые инструменты информации для начала, но остальныеЯ оставлю до вас, включая задачу преобразования значений A, B и C в их двоичные представления.

Как только вы узнаете двоичные шаблоны входов A и B в соответствии с их C выходами, и вы поймете таблицы истинности логических вентилей - таких операторов, как And - &, Or - |, Nand - (!&), Nor - (!|), Xor - ^ Xnor - (!^) и Not - !, а также комплимент (~).Вы должны быть в состоянии разработать свой алгоритм с эффективностью.

...