【发布时间】:2016-03-18 00:11:24
【问题描述】:
我们是否还需要在软件中模拟 128 位整数,或者这些天在您的普通桌面处理器中是否有硬件支持?
【问题讨论】:
-
我添加了 x86 标签,因为这是最常见的桌面指令集。如果您包括主要使用 ARM 的平板电脑和手机,您的问题将具有更广泛的吸引力。我怀疑 ARM 有 64 位 * 64 位到 128 位的指令。
我们是否还需要在软件中模拟 128 位整数,或者这些天在您的普通桌面处理器中是否有硬件支持?
【问题讨论】:
x86-64 指令集可以使用一条指令执行 64 位 * 64 位到 128 位(mul 表示无符号 imul 表示每个都有一个操作数)所以我认为在某种程度上x86 指令集确实包含对 128 位整数的一些支持。
如果您的指令集没有执行 64 位*64 位到 128 位的指令,那么您需要 several instructions to emulate this。
这就是为什么 128 位 * 128 位到更低的 128 位操作可以用 x86-64 的少量指令完成的原因。以 GCC 为例
__int128 mul(__int128 a, __int128 b) {
return a*b;
}
产生这个程序集
imulq %rdx, %rsi
movq %rdi, %rax
imulq %rdi, %rcx
mulq %rdx
addq %rsi, %rcx
addq %rcx, %rdx
它使用一个 64 位 * 64 位到 128 位指令,两个 64 位 * 64 位到低 64 位指令,以及两个 64 位加法。
【讨论】:
adc / sbb(进位加,借位减)和 div/idiv(128b/64b -> 64b 除数和 64b 余数)。所以加/减 128b 整数只需要两条指令(不计算数据移动)。按位布尔值可以在 SSE 向量中完成。
adc 支持 128 位,但我明白你的意思。一些指令集没有adc(例如 SSE/AVX),因此它们需要更多指令来进行多字加法。
我将通过将桌面处理器与简单的微控制器进行比较来解释它,因为算术逻辑单元 (ALU) 是 CPU 中的计算器,以及 Microsoft x64 Calling Convention 与 System-V Calling Convention 的类似操作.简短的答案滚动到最后,但长答案是通过比较 x86/x64 与 ARM 和 AVR 最容易看出区别:
原生双字整数乘法架构支持比较
| CPU | word x word => dword | dword x dword => dword |
|---|---|---|
| M0 | No (only 32x32 => 32) | No |
| AVR | 8x8 => 16 (some versions only) | No |
| M3/M4/A | Yes (32x32 => 64) | No |
| x86/x64 | Yes (up to 64x64 => 128) | Yes (up to 64x64 => 64 for x64) |
| SSE/SSE2/AVX/AVX2 | Yes (32x32 => 64 SIMD elements) | No (at most 32x32 => 32 SIMD elements) |
智能手机、PC 和服务器中的 CPU 具有多个 ALU,可对各种宽度的寄存器执行计算。另一方面,微控制器通常只有一个 ALU。 CPU 的字长与 ALU 的字长不同,尽管它们可能相同,Cortex-M0 就是一个很好的例子。
Cortex-M0 是一款 Thumb-2 处理器,它是一种紧凑型(主要是 16 位)指令编码,适用于 32 位架构。 (寄存器和 ALU 宽度)。 Cortex-M3/M4 有一些more instructions,包括smull/umull,32x32 => 64 位widening multiply,它们有助于提高精度。尽管存在这些差异,但所有 ARM CPU 共享相同的架构寄存器集,可轻松从 M0 升级到 M3/M4 和更快的 Cortex-A 系列智能手机处理器 NEON SIMD extensions。
当执行二进制操作时,值溢出寄存器是很常见的(即太大而无法放入寄存器)。 ALU 有 n 位输入和 n 位输出,带有一个进位(即溢出)标志。
加法不能在一条指令中执行,但需要的指令相对较少。但是,对于乘法,您需要将字长加倍以适应结果,当您需要 2n 个输出时,ALU 只有 n 个输入和 n 个输出,这样就无法工作。例如,通过将两个 32 位整数相乘,您需要一个 64 位的结果,而两个 64 位整数需要最多 128 位的结果以及 4 个字大小的寄存器; 2 还不错,但是 4 变得复杂,而且你的寄存器用完了。 CPU 处理此问题的方式将有所不同。对于 Cortex-M0,没有相关指令,但对于 Cortex-M3/M4,有一条用于 32x32=>64 位寄存器乘法的指令,需要 3 个时钟周期。
(您可以使用 Cortex-M0 的 32x32 => 32 位 muls 作为 16x16=>32 位构建块来进行更大的乘法运算;这显然效率低下,但可能仍然比手动移位和有条件相加要好。)
AVR 微控制器有 131 条指令,可在 32 个 8 位寄存器上运行,按寄存器宽度分类为 8 位处理器,但它同时具有 8 位和 16 位 ALU。 The AVR processor cannot do 16x16=>32-bit calculations 具有两个 16 位寄存器对或 64 位整数数学,无需软件破解。这与寄存器组织和 ALU 溢出操作中的 x86/x64 设计相反。这就是 AVR 被归类为 8/16 位 CPU 的原因。你为什么在乎?它会影响性能和中断行为。
AVR“小”,而其他没有“增强”指令集的设备根本没有硬件乘法。但如果完全支持,mul 指令是 8x8 => 16 位硬件乘法。 https://godbolt.org/z/7bbqKn7Go 展示了 GCC 如何使用它。
在 x86 上,可以使用 MUL 指令将两个 32 位整数相乘以创建一个 64 位整数,从而在 EDX:EAX 中生成无符号 64 位,或在 RDX:RAX 对中生成 128 位结果。
在 x86 上添加 64 位整数只需要两条指令(add/adc 感谢进位标志),对于 x86-64 上的 128 位整数也是如此。但
将两个寄存器整数相乘需要更多的工作。
例如,在 32 位 x86 上,64x64 => 64 位乘法 (long long) 需要 很多 指令,包括 3 次乘法(低 x 低加宽,叉积不是,因为我们不需要完整结果的高 64 位)。 Here is an example of 32x64=>64-bit x86 signed multiply assembly for x86:
movl 16(%ebp), %esi ; get y_l
movl 12(%ebp), %eax ; get x_l
movl %eax, %edx
sarl $31, %edx ; get x_h, (x >>a 31), higher 32 bits of sign-extension of x
movl 20(%ebp), %ecx ; get y_h
imull %eax, %ecx ; compute s: x_l*y_h
movl %edx, %ebx
imull %esi, %ebx ; compute t: x_h*y_l
addl %ebx, %ecx ; compute s + t
mull %esi ; compute u: x_l*y_l
leal (%ecx,%edx), %edx ; u_h += (s + t), result is u
movl 8(%ebp), %ecx
movl %eax, (%ecx)
movl %edx, 4(%ecx)
x86 支持将两个寄存器配对以存储完整的乘法结果(包括高半部分),但您不能使用这两个寄存器来执行 64 位 ALU 的任务。这是 x64 软件在 64 位或更宽的整数数学运算中比 x86 软件运行得更快的主要原因:您可以在一条指令中完成这项工作!您可以想象 x86 模式下的 128 位乘法运算会非常昂贵,it is。 The x64 is very similar to x86 except with twice the number of bits.
当 CPU 将 2 个字大小的寄存器配对以创建单个双字大小的值时,在堆栈上,生成的双字值将与 RAM 中的字边界对齐。除了两个寄存器对之外,四字数学是一种软件破解。这意味着对于 x64,可以组合两个 64 位寄存器来创建一个 128 位寄存器对溢出,该溢出与 RAM 中的 64 位字边界对齐,但 128x128=>128 位数学是软件破解。
然而,x86/x64 是superscalar CPU,而您所知道的寄存器仅仅是architectural registers。在幕后,有更多的寄存器可以帮助优化 CPU 流水线以使用多个 ALU 执行乱序指令。
SSE/SSE2 引入了 128 位 SIMD 寄存器,但没有指令将它们视为单个宽整数。 paddq 并行执行两个 64 位加法,但 no hardware support for 128-bit addition,甚至支持手动传播跨元素进位。最宽的乘法是两个并行的 32x32=>64 操作,是 x86-64 标量 mul 可以做的宽度的一半。请参阅Can long integer routines benefit from SSE? 了解最先进的技术,以及您必须跳过的障碍才能从 SSE/AVX 中获得非常大的整数的任何好处。
即使使用 AVX-512(对于 512 位寄存器),最宽的 add / mul 指令仍然是 64 位元素。 x86-64 确实在 SIMD 元素中引入了 64x64 => 64 位乘法。
C++ 应用程序处理 128 位整数的方式会因操作系统或裸机调用约定而异。微软有他们自己的约定,令我沮丧的是,生成的 128 位返回值 不能 从函数作为单个值返回。 Microsoft x64 Calling Convention 规定当返回一个值时,您可以返回一个 64 位整数或两个 32 位整数。例如,您可以使用 word * word = dword,但在 Visual-C++ 中,您必须使用 _umul128 来返回 HighProduct,无论它是否在 RDX:RAX 对中。我哭了,很难过。 :-(
但是,System-V 调用约定确实允许在 RAX:RDX 中返回 128 位返回类型。 https://godbolt.org/z/vdd8rK38e。 (并且 GCC / clang 有 __int128 让编译器发出必要的指令来 2 寄存器 add/sub/mul 和 div 的辅助函数 - Is there a 128 bit integer in gcc?)
至于您是否应该指望 128 位整数支持,很少遇到使用 32 位 x86 CPU 的用户,因为它们太慢了,因此设计软件在 32- 上运行并不是最佳实践位 x86 CPU,因为它会增加开发成本并可能导致用户体验下降;期望 Athlon 64 或 Core 2 Duo 达到最低规格。您可以预期代码在 Microsoft 上的性能不如 Unix 操作系统。
英特尔架构寄存器一成不变,但英特尔和 AMD 不断推出新的架构扩展,但编译器和应用程序需要很长时间才能更新,您不能指望它跨平台。你会想要阅读Intel 64 and IA-32 Architecture Software Developer’s Manual 和AMD64 Programmers Manual。
【讨论】:
rdx:rax 寄存器对中返回 128 位整数值(包括结构)。 godbolt.org/g/vhJyKE.
adds r0, r1 只需要 2 个字节的机器码,但在 32 位寄存器上运行。您认为需要两条 16 位指令来处理 32 位寄存器的建议不是 Thumb 模式的工作方式。您将操作数大小与指令宽度混淆了。
x 应该是乘法,那么带有 SSE/AVX/AVX2/AVX512 的 x86-64 没有 64x64 => 128 位乘法。 AVX512 有 64x64 => 64 个压缩乘法 (vpmullq),但 AVX2 最宽的整数乘法是 32x32 => 64 个元素。 (或者您可以使用 hack,例如使用 double-precision FMA 指令进行 52 位整数乘法:Can I use the AVX FMA units to do bit-exact 52 bit integer multiplications?。我认为 Prime95 会这样做。)
简短的回答是:不!
更详细地说,SSE 寄存器是 128 位宽的,但不存在将它们视为 128 位整数的指令。充其量,这些寄存器被视为两个 64 位(无)符号整数。可以通过并行添加这两个 64 位值并手动处理溢出来构造诸如加法/...之类的操作,但不能使用单个指令。实现它可能会变得非常复杂和“丑陋”,请看这里:
How can I add together two SSE registers
与使用 64 位通用寄存器的实现(软件中的“仿真”)相比,这对于每个基本操作都必须执行,其优势可能值得怀疑。另一方面,这种 SSE 方法的一个优点是,一旦实现,它也适用于 256 位整数 (AVX2) 和 512 位整数 (AVX-512),只需稍作修改。
【讨论】:
5 年后;这个问题的答案仍然是“否”。
具体来说,让我们将其分解为 80x86 的各种操作:
整数加法
不支持 128 位。一直支持“大于本机支持的”整数运算(例如add 然后adc)。几年前,通过引入 Intel ADX(多精度加进指令扩展)改进了对“比本机支持的更大”整数运算的支持,以便可以在保留其他标志的同时完成它(这在循环中可能很重要 -例如,其他标志控制退出条件的地方)。
整数减法
不支持 128 位。一直支持“大于本机支持的”整数运算(例如sub 然后sbb)。这没有改变(英特尔的 ADX 扩展不包括减法)。
整数乘法
不支持 128 位(将 128 位整数与 128 位整数相乘)。一直支持“大于本机支持的”整数运算(例如,将 64 位整数相乘并得到 128 位结果)。
整数除法
不支持 128 位(将 128 位整数除以 128 位整数)。一直部分支持“大于本机支持的”整数运算(例如,将 128 位整数除以 64 位整数并获得 64 位结果),当除数为 128 位时,这无济于事。
整数移位
不支持 128 位。一直支持“大于本机支持”的整数运算(例如 shld 和 shrd,以及 rcr 和 rcl)。
原子
大多不支持 128 位。有一条 lock cmpxchg16b 指令(在引入长模式后不久引入)可用于在单个指令中模拟 128 位“原子加载”或在单个指令中模拟 128 位“如果相等则进行原子比较”指令(“原子存储”和“原子交换”需要重试循环)。 注意:对齐的 SSE 和 AVX 加载/存储不能保证是原子的(实际上,对于所有读取和写入大小相同的简单情况,它们可能是“伪原子”,也可能不是“伪原子”)。
按位运算(AND、OR、XOR)
对于通用寄存器,没有。对于 SIMD,自 2000 年引入 SSE2 以来一直支持 128 位(因为在支持长模式和 64 位通用寄存器之前);但这将是一种有用的罕见场景(例如,您没有进行 128 位操作的混合,并且可以避免将值移入/移出 SSE 寄存器的成本)。
位域/位串操作(设置、清除和测试位域中的各个位)
“部分支持”。如果位域在 register/s 中,则不支持 128 位位域。如果位域在内存中,则 80x86 支持显着更大的尺寸(16 位代码中最多 65536 位位域,32 位代码中最多 4294967296 位位域等)。这包括对位域的原子操作(lock bts .. 等)。
寻址
不支持(物理地址或虚拟地址)。我们甚至都没有完整的 64 位地址。有一个“5 级分页”扩展可将虚拟地址从 48 位增加到 57 位,但很难热情(由于“有用性与开销”妥协)。
【讨论】:
bts [di], eax 始终可以在任何可以运行它的 CPU 上运行,即使在 16 位模式下也是如此。根据 16 位模式的风格,您的段限制可能为 65536 字节,但这是 524288 (512 Kib) 的位域限制。在实践中,具有寄存器位索引的内存目标 BTS 在手动寻址以加载/BTS/存储的现代 CPU 上较慢,因此它是一条 x86 指令,但硬件仅通过约 10 微码的微码处理它。跨度>
RISC-V 有一个 128b 的候选 ISA,RV128I。
https://github.com/brucehoult/riscv-meta/blob/master/doc/src/rv128.md
然而,此时它只是一个架构,并没有被冻结。
【讨论】: