Rust支持128位整数,这些使用数据类型i128
(无符号整数使用u128
)表示:
let a: i128 = 170141183460469231731687303715884105727;
Rust如何在64位系统上操作i128值,例如如何进行算术运算?
据我所知,该值无法适应x86-64 CPU的一个寄存器,那么编译器是否会使用两个寄存器来处理一个i128值?或者它们是使用某种大整数结构来表示它们的?
Rust支持128位整数,这些使用数据类型i128
(无符号整数使用u128
)表示:
let a: i128 = 170141183460469231731687303715884105727;
Rust如何在64位系统上操作i128值,例如如何进行算术运算?
据我所知,该值无法适应x86-64 CPU的一个寄存器,那么编译器是否会使用两个寄存器来处理一个i128值?或者它们是使用某种大整数结构来表示它们的?
add
这样的抽象指令的语义是由LLVM自身定义的。通常,具有单指令等价物的抽象指令将被编译为该本地指令,而那些没有的则将被模拟,可能使用多个本地指令。mcarton's answer演示了LLVM如何编译本地和模拟指令。i8
的add
指令可能使用更宽的指令进行模拟,额外的位被丢弃。)
在LLVM IR级别,答案都不是:编译器是否以某种方式使用两个寄存器表示一个
i128
值?或者他们使用一种大整数结构来表示它们?
i128
可以放在一个寄存器中,就像其他单值类型一样。另一方面,一旦翻译成机器码,两者之间实际上没有区别,因为结构体可以像整数一样分解成寄存器。但在进行算术运算时,LLVM很可能会将整个内容加载到两个寄存器中。
adc
,这使得执行扩展精度整数加/减相当高效。fn main() {
let a = 42u128;
let b = a + 1337;
}
在不进行优化的情况下,编译器在编译x86-64时生成以下内容:
(@PeterCordes添加了注释)
playground::main:
sub rsp, 56
mov qword ptr [rsp + 32], 0
mov qword ptr [rsp + 24], 42 # store 128-bit 0:42 on the stack
# little-endian = low half at lower address
mov rax, qword ptr [rsp + 24]
mov rcx, qword ptr [rsp + 32] # reload it to registers
add rax, 1337 # add 1337 to the low half
adc rcx, 0 # propagate carry to the high half. 1337u128 >> 64 = 0
setb dl # save carry-out (setb is an alias for setc)
mov rsi, rax
test dl, 1 # check carry-out (to detect overflow)
mov qword ptr [rsp + 16], rax # store the low half result
mov qword ptr [rsp + 8], rsi # store another copy of the low half
mov qword ptr [rsp], rcx # store the high half
# These are temporary copies of the halves; probably the high half at lower address isn't intentional
jne .LBB8_2 # jump if 128-bit add overflowed (to another not-shown block of code after the ret, I think)
mov rax, qword ptr [rsp + 16]
mov qword ptr [rsp + 40], rax # copy low half to RSP+40
mov rcx, qword ptr [rsp]
mov qword ptr [rsp + 48], rcx # copy high half to RSP+48
# This is the actual b, in normal little-endian order, forming a u128 at RSP+40
add rsp, 56
ret # with retval in EAX/RAX = low half result
在这里,您可以看到值42
存储在rax
和rcx
中。
(编辑注:x86-64 C调用约定将128位整数返回到RDX:RAX中。但是,此main
根本不返回任何值。所有多余的复制都是由于禁用优化,并且Rust实际上在调试模式下检查溢出。)
为了比较,在这里是Rust 64位整数在x86-64上的汇编代码,其中不需要使用带进位的加法,只需为每个值使用单个寄存器或堆栈插槽。
playground::main:
sub rsp, 24
mov qword ptr [rsp + 8], 42 # store
mov rax, qword ptr [rsp + 8] # reload
add rax, 1337 # add
setb cl
test cl, 1 # check for carry-out (overflow)
mov qword ptr [rsp], rax # store the result
jne .LBB8_2 # branch on non-zero carry-out
mov rax, qword ptr [rsp] # reload the result
mov qword ptr [rsp + 16], rax # and copy it (to b)
add rsp, 24
ret
.LBB8_2:
call panic function because of integer overflow
u128
参数并返回值的函数(例如此处https://godbolt.org/z/6JBza0),而不是禁用优化以防止编译器在编译时对常量参数进行常量传播,那么您会看到*简单得多*的代码(例如只有add / adc)。 - Peter Cordes是的,与32位机上的64位整数处理方式相同,或者16位机上的32位整数,甚至8位机上的16位和32位整数(仍适用于微控制器!)。是的,你把数字存储在两个寄存器、内存位置或其他位置(其实并不重要)。加法和减法很简单,只需要两个指令并使用进位标志即可。乘法需要三个乘法和一些加法(常见的64位芯片已经具有一个64x64->128的乘法操作,输出到两个寄存器)。除法...需要一个子程序,而且非常慢(除了某些情况外,将常数除以移位或乘法可能会转换成),但它仍然有效。按位与/或/异或只需分别在上半部分和下半部分执行即可。移位可以通过旋转和屏蔽来完成。这基本上涵盖了所有东西。
为了提供更清晰的例子,在x86_64上使用-O
标志编译的函数
pub fn leet(a : i128) -> i128 {
a + 1337
}
编译为
example::leet:
mov rdx, rsi
mov rax, rdi
add rax, 1337
adc rdx, 0
ret
我的原始帖子中使用的是u128
,而不是您询问的i128
。不管用哪种方式,该函数都会编译相同的代码,这证明在现代CPU上,有符号和无符号加法是相同的。
另一个列表生成的是未经优化的代码。它可以安全地在调试器中单步执行,因为它确保您可以在程序的任何行上设置断点并检查任何变量的状态。它比较慢且难以阅读。优化版本更接近实际运行的代码。
此函数的参数a
以一对64位寄存器rsi:rdi的形式传递。结果返回到另一对寄存器rdx:rax中。前两行代码将总和初始化为a
。
第三行将1337添加到输入的低位单词中。如果溢出,则在CPU的进位标志中进行记录。第四行将零添加到输入的高位以及(如果有进位)再加1。
您可以将其视为将一位数字加到两位数字中的简单加法。
a b
+ 0 7
______
但在基数18,446,744,073,709,551,616中。您仍然首先添加最低“数字”,可能会向下一列进位,然后添加下一个数字加上进位。减法非常相似。
乘法必须使用恒等式(2⁶⁴a + b)(2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴(ad+bc) + bd,其中每个乘法在一个寄存器中返回产品的上半部分,在另一个寄存器中返回产品的下半部分。其中一些术语将被删除,因为第128位以上的位不适合u128
并且被丢弃。即使如此,这需要多个机器指令。除法也需要几个步骤。对于有符号值,乘法和除法还需要转换操作数和结果的符号。这些操作效率都不高。
在其他架构中,情况会变得更容易或更难。RISC-V定义了一个128位指令集扩展,尽管据我所知,没有人在硅片中实现它。在没有这个扩展的情况下,RISC-V架构手册建议使用条件分支:addi t0,t1,+imm; blt t0,t1,overflow
SPARC具有类似于x86的控制代码,但您必须使用特殊指令add,cc
来设置它们。另一方面,MIPS要求您检查两个无符号整数之和是否严格小于其中一个操作数。如果是这样,则加法溢出。至少,您可以在不需要条件分支的情况下将另一个寄存器设置为进位比特的值。
sub
结果的高位来检测两个无符号数字哪个更大,您需要对于n
位输入,一个n+1
位的sub
结果。也就是说,您需要查看进位而不是相同宽度结果的符号位。这就是为什么x86无符号分支条件基于CF(完整逻辑结果的第64或32位),而不是SF(第63或31位)。 - Peter Cordesx-(a*b)
操作并从被除数、商和除数计算余数(即使使用乘法逆元对除法部分进行常量除数也很有用)。我之前没有听说过将div+mod指令融合成单个divmod操作的ISA; 这很不错。 - Peter Cordesmul r64
是2个uop,其中第二个会写入RDX高半部分)。 - Peter Cordesadc
、sbb
和cmov
解码为2个微操作。 (Haswell引入了3个输入微操作用于FMA,Broadwell将其扩展到整数。) - Peter Cordes