现代处理器是否支持 128 位整数?

Is there hardware support for 128bit integers in modern processors?

我们是否仍然需要在软件中模拟 128 位整数,或者现在普通台式机处理器是否有硬件支持它们?

简短的回答是:不!

更详细地说,SSE 寄存器是 128 位宽,但不存在将它们视为 128 位整数的指令。充其量,这些寄存器被视为两个 64 位(无)符号整数。像 addition/... 这样的操作可以通过并行添加这两个 64 位值并手动处理溢出来构造,但不能使用单个指令。实现这一点可能会非常复杂,"ugly",请看这里:

How can I add together two SSE registers

与使用 64 位通用寄存器("emulation" 在软件中)的实现相比,每个基本操作都必须这样做,这可能具有值得怀疑的优势。另一方面,这种 SSE 方法的一个优点是,一旦实施,它也适用于 256 位整数 (AVX2) 和 512 位整数 (AVX-512),只需稍作修改。

x86-64 指令集可以使用一条指令执行 64 位 * 64 位到 128 位(mul 用于无符号 imul 用于带符号的每个操作数)所以我会认为在某种程度上 x86 指令集确实包括对 128 位整数的一些支持。

如果你的指令集没有64位*64位转128位的指令那么你需要.

这就是为什么 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 位加法。

我将通过将桌面处理器与简单的微控制器进行比较来解释它,因为算术逻辑单元 (ALU) 的操作相似,它们是 CPU 中的计算器,Microsoft x64 Calling Convention vs the 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)

如果您看懂了这张图表,请跳至简答

CPUs in smartphones, PCs, and Servers 有多个 ALU 对各种宽度的寄存器执行计算。另一方面,微控制器通常只有一个 ALU。 CPU 的字长与 ALU 的字长不同,尽管它们可能相同,Cortex-M0 就是一个很好的例子。

ARM 架构

Cortex-M0 是 Thumb-2 处理器,它是针对 32 位架构的紧凑型(主要是 16 位)指令编码。 (寄存器和 ALU 宽度)。 Cortex-M3/M4 有一些 more instructions, including smull / umull, 32x32 => 64-bit widening multiply that are helpful for extended precision. Despite these differences, all ARM CPUs share the same set of architectural registers, which is easy to upgrade from M0 to M3/M4 and faster Cortex-A series smartphone processors with NEON SIMD extensions.

ARM 架构寄存器

执行二进制运算时,值溢出寄存器很常见(即变得太大而无法放入寄存器)。 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 架构

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 如何使用它。

AVR 架构寄存器

x86 架构

在 x86 上,可以使用 MUL 指令将两个 32 位整数相乘以创建一个 64 位整数,从而在 EDX:EAX 中生成无符号 64 位整数,或在 [ 中生成 128 位整数=129=]对.

在 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 , %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. .

x86 架构寄存器

x64 架构寄存器

当CPUs 对 2 个字大小的寄存器创建单个双字大小的值时,在堆栈上生成的双字值将与 RAM 中的字边界对齐。除了两个寄存器对之外,四字数学是一种软件破解。这意味着对于 x64,两个 64 位寄存器可以组合起来创建一个 128 位寄存器对溢出,该溢出与 RAM 中的 64 位字边界对齐,但是 128x128=>128 位数学是一个软件 hack。

然而x86/x64是一个superscalar CPU, and the registers you know of are merely the architectural registers。在幕后,还有更多寄存器可帮助优化 CPU 管道以使用多个 ALU 执行乱序指令。

SSE/SSE2 引入了 128 位 SIMD registers, but no instructions treat them as a single wide integer. There's paddq that does two 64-bit additions in parallel, but no hardware support for 128-bit addition, or even support for manually propagating carry across elements. The widest multiply is two 32x32=>64 operations in parallel, half the width of what you can do with x86-64 scalar mul. See Can long integer routines benefit from SSE? 作为最先进的技术,对于非常大的整数,您必须跳过这些障碍才能从 SSE/AVX 中获得任何好处。

即使使用 AVX-512(对于 512 位寄存器),最宽的加/乘指令仍然是 64 位元素。 x86-64 确实引入了 64x64 => SIMD 元素中的 64 位乘法。

简答

C++ 应用程序处理 128 位整数的方式将根据操作系统或裸机调用约定而有所不同。 Microsoft 有自己的约定,令我感到沮丧的是,生成的 128 位 return 值 不能 从函数中 return 编辑为单个值. Microsoft x64 Calling Convention dictates that when returning a value, you may return one 64-bit integer or two 32-bit integers. For example, you can do word * word = dword, but in Visual-C++ you must use _umul128 到 return HighProduct,不管它在 RDX:RAX 对中。我哭了,很难过。 :-(

System-V calling convention, however, does allow for returning 128-bit return types in RAX:RDX. https://godbolt.org/z/vdd8rK38e. (And GCC / clang have __int128 to get the compiler to emit the necessary instructions to 2-register add/sub/mul, and helper function for div - Is there a 128 bit integer in gcc?)

至于是否应该依靠 128 位整数支持,很少遇到使用 32 位 x86 CPU 的用户,因为它们太慢了,所以这不是设计的最佳实践32 位 x86 CPUs 上 运行 的软件,因为它会增加开发成本并可能导致用户体验下降;期望 Athlon 64 或 Core 2 Duo 达到最低规格。您可以预期代码在 Microsoft 上的性能不如 Unix OS(s).

英特尔架构寄存器是一成不变的,但英特尔和 AMD 不断推出新的架构扩展,但编译器和应用程序需要很长时间才能更新,您不能指望跨平台。您需要阅读 Intel 64 and IA-32 Architecture Software Developer’s Manual and AMD64 Programmers Manual.

5年后;这个问题的答案仍然是“否”。

具体来说,我们将其分解为针对 80x86 的各种操作:

整数加法

不支持 128 位。一直支持“大于本机支持”的整数运算(例如 add 然后 adc)。几年前,通过引入 Intel ADX(多精度加进位指令扩展)改进了对“大于本机支持”整数运算的支持,因此可以在保留其他标志(这在循环中可能很重要 -例如,其他标志控制退出条件的地方)。

整数减法

不支持 128 位。一直支持“大于本机支持”的整数运算(例如 sub 然后 sbb)。这没有改变(英特尔的 ADX 扩展不包括减法)。

整数乘法

不支持 128 位(将 128 位整数与 128 位整数相乘)。一直支持“大于本机支持”的整数运算(例如,乘以 64 位整数并获得 128 位结果)。

整数除法

不支持 128 位(将 128 位整数除以 128 位整数)。总是部分支持“大于本机支持”的整数运算(例如,将 128 位整数除以 64 位整数并获得 64 位结果),这在除数为 128 位时无济于事。

整数移位

不支持 128 位。一直支持“大于本机支持”的整数运算(例如 shldshrd,以及 rcrrcl)。

原子

大多不支持 128 位。有一条 lock cmpxchg16b 指令(在引入长模式后不久引入)可用于在一条指令中模拟 128 位“原子加载”或在单个指令(“原子存储”和“原子交换”需要重试循环)。 注意:不保证对齐的 SSE 和 AVX loads/stores 是原子的(实际上,对于所有读写大小相同的简单情况,它们可能是也可能不是“伪原子”) .

位运算(与、或、异或)

对于通用寄存器,没有。对于 SIMD,自 2000 年推出 SSE2 以来一直支持 128 位(因为之前支持长模式和 64 位通用寄存器);但这在极少数情况下有用(例如,您没有混合执行 128 位操作并且可以避免移动值 to/from SSE 寄存器的成本)。

Bitfield/Bit 字符串操作(设置、清除和测试位域中的各个位)

“部分支持”。如果位域在 register/s 中,则不支持 128 位位域。如果位域在内存中,那么 80x86 已经支持更大的年龄大小(16 位代码中最多 65536 位位域,32 位代码中最多 4294967296 位位域,等等)。这包括位域上的原子操作(lock bts ..,等等)。

寻址

不支持(物理地址或虚拟地址)。我们甚至还没有完整的 64 位地址。有一个“5 级分页”扩展,将虚拟地址从 48 位增加到 57 位,但很难热情(由于“实用性与开销”的折衷)。

RISC-V 有一个用于 128b 的候选 ISA,RV128I。

https://github.com/brucehoult/riscv-meta/blob/master/doc/src/rv128.md

但是,此时它只是一个架构,并没有冻结。