我有以下 Intel PCLMULQDQ intrinsic (无进位乘法):
__m128i a, b; // Set to some value
__m128i r = _mm_clmulepi64_si128(a, b, 0x10);
0x10
告诉我乘法是:r = a[63:0] * b[127:64]
我需要将其转换为 NEON(或更准确地说,使用 Crypto 扩展名):
poly64_t a, b; // Set to some value
poly16x8_t = vmull_p64(...) or vmull_high_p64(...);
我认为
vmull_p64
在低 64 位上工作,而 vmull_high_p64
在高 64 位上工作。我想我需要将值之一移动 128 位值来模仿 _mm_clmulepi64_si128(a, b, 0x10)
。 PMULL, PMULL2 (vector) 的文档不太清楚,我不确定结果会是什么,因为我不理解 2 的排列说明符。 ARM ACLE 2.0 也没有太大帮助:如何将
_mm_clmulepi64_si128
转换为 vmull_{high}_p64
?对于任何考虑投资 NEON、PMULL 和 PMULL2 的人... 64 位乘法器和多项式支持是值得的。基准测试显示 GMAC 的 GCC 代码从 12.7 cpb 和 90 MB/s (C/C++) 下降到 1.6 cpb 和 670 MB/s(NEON 和 PMULL{2})。
最佳答案
由于您通过评论澄清了混淆的来源:
一个完整的乘法产生的结果是输入宽度的两倍。 add 最多可以产生一个进位位,但 mul 产生整个上半部分。
乘法完全等同于移位 + 加法,这些移位使一个操作数中的位高达 2N - 1(当输入为 N 位宽时)。见 Wikipedia's example 。
在像 x86's mul
instruction 这样的普通整数乘法(在加法步骤中有进位)中,部分和的进位可以设置高位,因此结果正好是两倍宽。
XOR 是没有进位的加法,因此无进位乘法是相同的移位加法算法,但使用 XOR 而不是加进位。在无进位乘法中,没有进位,因此全角结果的最高位始终为零。英特尔甚至在 pclmuludq
的 x86 insn 引用手册的 Operation 部分明确说明了这一点: DEST[127] ← 0;
。该部分精确地记录了产生结果的所有移位和异或。PMULL[2]
文档对我来说似乎很清楚。目标必须是 .8H
vector (这意味着八个 16 位(半字)元素)。 PMULL
的源必须是 .8B
vector (8 个 1 字节元素),而 PMULL2
的源必须是 .16B
(16 个 1 字节元素,其中仅使用每个源的前 8 个元素)。
如果这是 ARM32 NEON,其中每个 16B vector 寄存器的上半部分是奇数编号的窄寄存器,那么 PMULL2
将无用处。
但是,没有“操作”部分来准确描述哪些位与哪些其他位相乘。幸运的是, paper linked in comments 很好地总结了适用于 ARMv7 和 ARMv8 32 位和 64 位的可用指令 。 .8B/.8H 组织说明符似乎是假的,因为 PMULL
确实像 SSE 的 pclmul 指令一样执行单个 64x64 -> 128 无进位 mul。 ARMv7 VMULL.P8
NEON insn 确实做了一个打包的 8x8->16,但明确表示 PMULL
(和 ARMv8 AArch32 VMULL.P8
)是不同的。
ARM 文档没有说任何这些太糟糕了;它似乎非常缺乏,尤其是。重新误导 .8B
vector 组织的东西。那篇论文展示了一个使用预期的 .1q
和 .1d
(和 .2d
)组织的例子,所以也许汇编器并不关心你认为你的数据意味着什么,只要它的大小合适。
要进行高低相乘,您需要移动其中一个。
例如, 如果您需要所有四种组合 (a0*b0, a1*b0, a0*b1, a1*b1),就像您构建 128x128 -> 128 乘以 64x64 -> 128 乘法(使用 Karatsuba ),你可以这样做:
pmull a0b0.8H, a.8B, b.8B
pmull2 a1b1.8H, a.16B, b.16B
swap a's top and bottom half, which I assume can be done efficiently somehow
pmull a1b0.8H, swapped_a.8B, b.8B
pmull2 a0b1.8H, swapped_a.16B, b.16B
因此,看起来 ARM 的设计选择包括下下和上上,但不包括交叉乘法指令(或像 x86 那样的选择器常量)并不会导致效率低下。而且由于 ARM 指令不能像 x86 的可变长度机器编码那样添加额外的立即数,所以这可能不是一个选项。
同样的事情的另一个版本,有一个真正的 shuffle 指令和 Karatsuba 之后(从 Implementing GCM on ARMv8 逐字复制)。但仍然是编造的寄存器名称。这篇论文在此过程中重复使用了相同的临时寄存器,但我已经按照我为 C 内在函数版本命名的方式命名了它们。这使得扩展精度乘法的操作非常清楚。编译器可以为我们重用死寄存器。
1: pmull a0b0.1q, a.1d, b.1d
2: pmull2 a1b1.1q, a.2d, b.2d
3: ext.16b swapped_b, b, b, #8
4: pmull a0b1.1q, a.1d, swapped_b.1d
5: pmull2 a1b0.1q, a.2d, swapped_b.2d
6: eor.16b xor_cross_muls, a0b1, a1b0
7: ext.16b cross_low, zero, xor_cross_muls, #8
8: eor.16b result_low, a0b0, cross_low
9: ext.16b cross_high, xor_cross_muls, zero, #8
10: eor.16b result_high, a1b1, cross_high
关于将 _mm_clmulepi64_si128 转换为 vmull_{high}_p64,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38553881/