伽罗瓦域 GF(4) 中乘法的常数时间
Posted
技术标签:
【中文标题】伽罗瓦域 GF(4) 中乘法的常数时间【英文标题】:Constant time for multiplication in Galois Field GF(4) 【发布时间】:2021-09-06 09:56:14 【问题描述】:Tl;dr:有没有办法以任何方式(包括多线程)改进下面的代码,因为代码将运行数千亿次?
目标是找到用于在伽罗瓦域 GF(4) 中执行乘法的恒定时间算法(没有 for 循环)。我不确定这是否可能,但值得一试。
一些背景:GF(2) 或以 2 为底的乘法相当于 anding 两个值相乘。这是因为:
a | b | a × b = a ∧ b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
例如:
10101011010100 × 10011000101101 =
10101011010100
10011000101101 ∧
--------------
10001000000100
当涉及到 GF(4) 时,可以使用四种不同的符号:0、1、2 和 3。它不与以 4 为底执行乘法相同,因为有些与其他数字相乘时,数字不会给出预期的结果。它们在下表中以粗体显示:
a | b | a × b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
0 | 2 | 0 |
0 | 3 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
1 | 2 | 2 |
1 | 3 | 3 |
2 | 0 | 0 |
2 | 1 | 2 |
2 | 2 | 3 |
2 | 3 | 1 |
3 | 0 | 0 |
3 | 1 | 3 |
3 | 2 | 1 |
3 | 3 | 2 |
上表更简洁的形式可以用下面的乘法表来概括:
× | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 2 | 3 |
2 | 0 | 2 | 3 | 1 |
3 | 0 | 3 | 1 | 2 |
我们可以将四个数字中的每一个都写成二进制,因为乘法将在值的二进制表示上执行:
Digit | Binary representation |
---|---|
0 | 00 |
1 | 01 |
2 | 10 |
3 | 11 |
在 GF(4) 中,乘法是通过逐位相乘来完成的,没有进位。例如:
21302032 × 31012233 =
21302032
31012233 ×
--------
11003021
我们可以使用值的二进制表示并执行乘法:
21302032 = 1001110010001110 in binary
31012233 = 1101000110101111 in binary
11003021 = 0101000011001001 in binary
1001110010001110
1101000110101111 ×
----------------
0101000011001001
最后,这是一个执行乘法的工作 java 代码的实现。但是,它使用 for 循环,目标是提出恒定时间算法:
public class Multiplication
public static void main(String[] args)
final byte[][] MUL_ARRAY = new byte[][]
0, 0, 0, 0,
0, 1, 2, 3,
0, 2, 3, 1,
0, 3, 1, 2
;
long mask;
byte shift = 2;
//long is 64 bits which means it can store 32 digits quaternary value.
int n = 8; //# of quaternary digits (ALWAYS given)
long v1 = 0b1001110010001110;//21302012 in base 4
long v2 = 0b1101000110101111;//31012233 in base 4
long result = 0;
for (int i = 0; i < n; i++)
//get far-right quaternary digit of the two vectors:
mask = 0b11;
mask = mask << 2 * (n - i - 1);
long v1DigitPadded = v1 & mask;//correct digit with zero padding
long v2DigitPadded = v2 & mask;//correct digit with zero padding
//shift the digits so that the digit needed is at far-right
v1DigitPadded = v1DigitPadded >>> 2 * (n - i - 1);
v2DigitPadded = v2DigitPadded >>> 2 * (n - i - 1);
//The actual quaternary digit
byte v1Digit = (byte) v1DigitPadded;
byte v2Digit = (byte) v2DigitPadded;
byte product = MUL_ARRAY[v1Digit][v2Digit];
long resultDigit = product << 2 * (n - i - 1);
result = result | resultDigit;
//desired output: 0101000011001001
//prints the value in binary with zeros padding on the left
String s = Long.toBinaryString(result);
String output = String.format("%" + n * 2 + "s", s).replace(" ", "0");
System.out.println("The output is: " + output);
有算法吗?如果没有,是否有一些改进可以帮助我的逻辑(可能是一种高效的多线程方法)?
【问题讨论】:
由于循环大小是恒定的,你的算法是恒定的时间... 【参考方案1】:“可以使用公式(没有 for 循环)来实现结果吗?”问题的答案是的。
这里有一些代码。它在 Python 中,但它应该直接转换为 Java,无需进行重大修改。示例和解释遵循代码。它假设 64 位整数编码 GF(4) 的 32 个元素 - 如果您想要 32 位整数,请改用 MASK
或 0x5555_5555
。
#: Mask to extract the low-order bit of each GF(4) element.
MASK = 0x5555_5555_5555_5555
def gf4_vector_multiply(i, j):
"""
Vector multiply in GF(4).
i and j are 64-bit integers, each of which encodes 32 elements
of GF(4) in pairs of consecutive bits.
The returned value represents the 32 products of the GF(4) elements,
encoded as a 64-bit integer in the same way.
"""
ii = i >> 1
jj = j >> 1
r0 = (ii & jj) ^ (i & j)
r1 = (ii & j) ^ (jj & i) ^ (ii & jj)
return ((r1 & MASK) << 1) ^ (r0 & MASK)
示例
这是应用于您给出的示例的上述函数,将 21302032 和 31012233 相乘得到 11003021:
>>> i = int("21302032", base=4)
>>> j = int("31012233", base=4)
>>> product = gf4_vector_multiply(i, j)
>>> product == int("11003021", base=4)
True
解释
我们分别计算每个产品的低位(上面代码中的r0
)和高位(r1
),然后将它们组合起来。
对于产品的低位,我们有下表(从问题中的表格复制,但将2s和3s替换为0s和1s):
× | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 1 |
2 | 0 | 0 | 1 | 1 |
3 | 0 | 1 | 1 | 0 |
此表中的值是以下两个表中相应值的按位异或:
× | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 1 |
2 | 0 | 0 | 0 | 0 |
3 | 0 | 1 | 0 | 1 |
和
× | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 1 |
3 | 0 | 0 | 1 | 1 |
这两个表中的第一个只是输入的低位的按位与(代码中的i & j
),而第二个是输入的高位的按位与(代码中的ii & jj
)。所以(ii & jj) ^ (i & j)
给了我们结果的低位。高位r1
的构造类似。
请注意,r0
和 r1
的有趣部分在每一对的低位中,即在 r0 & 0x55555555
和 r1 & 0x55555555
中。每对的高位(r0 & 0xaaaaaaaa
和r1 & 0xaaaaaaaa
)没有用:它们的值包含混合连续 GF(4) 位的结果,这不是我们想要的,所以我们必须小心在重新组装结果之前屏蔽这些位。
优化
我没有做太多努力来最小化这里的按位运算的数量;可能有一些空间可以减少位操作的总数。上面的代码使用了 14 个操作。这是一个不太清楚的变体,但使用了 12 个操作。请注意,使用正常的运算符优先规则(在 Java 和 Python 中),除了一对括号外,所有括号都是多余的:我发现带有括号 的代码更具可读性,但是,如果您认为这样看起来更好,可以随意将它们去掉。
def gf4_vector_multiply(i, j):
"""
Vector multiply in GF(4).
i and j are 64-bit integers, each of which encodes 32 elements
of GF(4) in pairs of bits.
The returned value represents the 32 products of the GF(4) elements,
encoded in the same way.
"""
ii = (i >> 1) & MASK
jj = (j >> 1) & MASK
return (((ii & j) ^ (jj & i)) << 1) ^ (ii & jj) ^ (i & j)
另外,正如您所建议的,如果您遇到令人尴尬的并行问题,那么使用多线程可能会有所帮助,但我认为这确实是对单独的 Stack Overflow 问题的调查。在现代 CPU 上,很可能上述代码足够简单,瓶颈将是数据进出 RAM 而不是计算。
特例 n = 1
在 cmets 中,作者提出了有关个位数乘法的问题。对于一位数的乘法,还有其他可用的技巧。这是一种可能性(同样在 Python 中,但它应该直接转换为 Java):
def gf4_scalar_multiply(i, j):
return (0x813e4 >> 2*i*j) & 3
在这里,我们简单地将乘积值编码成魔术常数0x813e4
中的位对,它的底数为2001033210
。现在我们使用移位来查找给定整数乘积的适当的以 4 为底的乘积。
让我们检查一下这是否给出了预期的乘法表:
>>> for i in range(4):
... for j in range(4):
... print(gf4_scalar_multiply(i, j), end=" ")
... print()
...
0 0 0 0
0 1 2 3
0 2 3 1
0 3 1 2
【讨论】:
感谢您的天才回答!这非常聪明!我有一个很好的后续问题:是否有更优化的逻辑来将一个数字乘以另一个数字(即当 n=1 时)? @M.AlJumaily:我为n = 1
的案例添加了一个可能的技巧。可能还有很多其他人。
谢谢你!我想了解更多关于这些二进制操作的知识,你有什么教科书可以推荐!?我对答案很满意,我没有任何其他问题或后续问题。我有一个想法,我可能在遥远的将来写一篇论文,这个解决方案将成为其中的一部分(请对此持保留态度)。有没有办法联系您以便在致谢中提及您?
@M.AlJumaily:对于类似的比特黑客,我推荐 Sean Anderson 的 website 和 Henry Warren 的优秀书籍 Hacker's Delight。以上是关于伽罗瓦域 GF(4) 中乘法的常数时间的主要内容,如果未能解决你的问题,请参考以下文章
论文翻译:未通过预选计算的椭圆曲线上的快速乘法(GF(2^m))