【问题标题】:Is it possible to efficiently compute A % B without having to compute A / B?是否可以有效地计算 A % B 而无需计算 A / B?
【发布时间】:2019-08-29 15:10:20
【问题描述】:

我正在用 C++ 编写一个多精度库,使用 2^64 的基数,目前我正在处理 mod 操作。我正在使用 Donald E. Knuth 的 1998 年版“计算机编程艺术”卷中描述的算法 D。 2,第 4.3.1 节,用于除法,产生商和余数。对于mod 操作,我正在执行除法,最后丢弃商。虽然 Knuth 的算法 D 如果在 C++ 中实现非常快,并在每个步骤中对部分除法和并发多精度乘法/减法进行了一些 ASM 增强,但我不确定是否有更好的方法,因为丢弃了一个精心计算的结果对我来说似乎效率不高。

不幸的是,在算法 D 中无法摆脱部分除法,因为通过迭代地从被除数中减去部分商和除数的乘积,需要部分商来计算余数。

我在互联网上搜索了替代解决方案,并找到了Paul BarrettPeter L. Montgomery 撰写的有影响力的论文。然而,他们使用的花哨的技巧似乎只有在以相同模数连续执行大量mod 操作时才会得到回报,因为它们涉及大量的预计算。在模幂运算等复杂运算中就是这种情况,其中单个模数需要多个平方和乘积的modBarrett 从余数的基本定义r = a - b * (a / b) 开始,并将除法更改为与b 倒数的乘法。然后他提出了一种计算这种乘法的有效方法,如果对几个类似的计算计算一次倒数,就会得到回报。 Montgomery 将操作数转换为完全不同的剩余系统,其中模运算成本低,但要付出来回转换的代价。

此外,两种算法都引入了一些限制,需要满足这些限制才能正确操作。例如,蒙哥马利通常要求操作数是奇数,这在使用素数的 RSA 计算中就是这种情况,但不能在一般情况下假设。在这些限制之外,还需要更多的标准化开销。

所以我需要的是一个高效的一次性mod 函数,没有开销和特殊限制。因此我的问题是:是否可以在不计算商的情况下计算余数,以一种比除法更有效的方式?

【问题讨论】:

  • 这个问题可能更适合CS.SE。如果你决定在那里发帖,你应该在这里删除你的帖子。也就是说,我不确定如何在不计算除数进入被除数的次数的情况下计算余数。
  • @FrançoisAndrieux:编译器没有多精度 mod% 操作,所以不,编译器不会自动高效地(或根本不会)编写他的函数。跨度>
  • @SBS 我认为您的理解是可靠的。针对特定情况存在特殊解决方案。但据我所知,没有普遍适用的通用解决方案。然而,与除法本身的成本相比,反向乘法的成本通常很小,这需要显着更多的乘法,即使您使用诸如哈雷迭代之类的东西作为倒数(具有三次收敛)作为基础。
  • @SBS 首要任务是构建可能的最快乘法(根据位长,适当使用 Karatsuba、Toom-Cook、FFT)。然后您可以使用它通过倒数的迭代计算(具有三次收敛的哈雷迭代)来实现除法,正如我在this question 中为udiv64 展示的那样。我记得,来自 Knuth 的算法 D 只是使用高基数的长手除法,它具有线性收敛性。
  • @SBS Yacas Book of Algorithms 似乎表明二阶迭代(牛顿迭代)是计算倒数的最佳选择,而不是我建议的三阶迭代(哈雷迭代)。我从 快速 细读中收集到的最佳信息,他们将最佳定义为需要最少数量的基本乘法运算。

标签: c++ algorithm modulus integer-division


【解决方案1】:

一个建议是编写一个简单的函数来计算A%B=C 并将ABC 值存储到一个数组中,然后将所有结果存储到一个向量中。然后将它们打印出来以查看所有输入和输出值的关系。

可以做一件事来简化其中的一些工作,那就是了解 mod 函数的一些属性。这两个语句将帮助您了解该功能。

 0 mod N = 0
 N mod 0 = undefined

由于0 mod N = 0 我们可以为A 放置一个测试用例,如果它是0,我们可以使用它来填充我们的数组。同样,如果B = 0 我们可以用-1 填充我们数组的C 值只是为了表示未定义,因为您不能执行A mod 0,因为编译将由于除以0而失败。

我写这个函数就是为了做到这一点;然后我从[0,15]AB 循环运行它。

#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 &gt; A 那么B == A。最后我们可以看到在AB &lt; Aoddeven 值之间存在模式。如果你能理解这些模式,那么其中大部分就变成了代数操作。从这里开始,下一步将是创建一个算法,该算法将获取所有这些数据并将其转换为其二进制等价物。

我选择上面N 的值作为15 是有原因的。这是由于二进制数字的所有可能组合在它们再次重复之前的二进制表示。我们知道单字节数据是 8 位;我们知道 [0,15] 中的值将适合其中的一半;例如:

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

在这 15 个不同的 0 和 1 序列之后,这些模式将重复。因此,通过上表,您可以将它们转换为二进制表示。现在,一旦您检查了A &amp; B 输入及其C 二进制输出的表示,并了解了我上面提到的结果的三个属性;您应该能够设计一种算法来快速计算任何 A B 组合的模数很容易。要记住的一个技巧是,还有 3 件事需要考虑。首先是eerokia用户所说的:

"特别是,2 次方的模可以用位运算代替。"

接下来是偶数或奇数的值,因为偶数和奇数的情况在B &lt; A 时确实呈现出不同的A mod B 模式。

我已经为您提供了一些信息工具以供您开始使用,但剩下的由您来完成,包括将ABC 值转换为它们的二进制表示的任务。

一旦您根据C 输出了解AB 输入的二进制模式,并了解逻辑门的真值表 - 运算符,例如And - &amp;Or - |、@ 987654365@,Nor - (!|)Xor - ^Xnor - (!^)Not - !以及恭维(~)。您应该能够高效地设计算法。

【讨论】:

  • 我已经向 OP 解释了一种他们可以用来设计算法的方法,并为他们提供了一些关于如何理解模运算相对于其输入和输出值的提示。然后从二进制数字表示中考虑这些值的关系,从那里他们可以使用位操作来使他们的算法尽可能高效,我什至解释了A 的奇数和偶数之间的不同情况。以及当AB 相等以及B 大于A 时的输出行为。我仍然投了反对票……
  • 请注意,我没有对你投反对票。基本上,您的建议是利用琐碎的案例。当然,在尝试 Knuth 的算法 D 之前,我已经在我的除法函数中这样做了,所以它在 mod 函数中是免费的:如果 B==0 -> 除以零。如果A==B -> q=1, r=0。如果A&lt;B -> q=0, r=A。如果B==1 -> q=a, r=0。如果B&lt;2^64 -> 快速除以 64 位数字。如果bitlength(A)==bitlength(B) -> q=1, r=A-B。否则Algorithm D。特别是A&lt;B 排除了大量琐碎的情况。但是,我不知道您在奇数和偶数方面看到了什么模式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-19
  • 2015-12-05
  • 2012-01-19
  • 1970-01-01
  • 2020-12-17
  • 1970-01-01
相关资源
最近更新 更多