How does Rust's 128-bit integer `i128` work on a 64-bit system?

前端 未结 4 798
北荒
北荒 2021-01-30 19:16

Rust has 128-bit integers, these are denoted with the data type i128 (and u128 for unsigned ints):

let a: i128 = 1701411834604692317316         


        
4条回答
  •  独厮守ぢ
    2021-01-30 19:42

    The compiler will store these in multiple registers and use multiple instructions to do arithmetic on those values if needed. Most ISAs have an add-with-carry instruction like x86's adc which makes it fairly efficient to do extended-precision integer add/sub.

    For example, given

    fn main() {
        let a = 42u128;
        let b = a + 1337;
    }
    

    the compiler generates the following when compiling for x86-64 without optimization:
    (comments added by @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
    

    where you can see that the value 42 is stored in rax and rcx.

    (editor's note: x86-64 C calling conventions return 128-bit integers in RDX:RAX. But this main doesn't return a value at all. All the redundant copying is purely from disabling optimization, and that Rust actually checks for overflow in debug mode.)

    For comparison, here is the asm for Rust 64-bit integers on x86-64 where no add-with-carry is needed, just a single register or stack-slot for each value.

    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
    

    The setb / test is still totally redundant: jc (jump if CF=1) would work just fine.

    With optimization enabled, the Rust compiler doesn't check for overflow so + works like .wrapping_add().

提交回复
热议问题