[rust] Rust의 128 비트 정수`i128`은 64 비트 시스템에서 어떻게 작동합니까?

Rust는 128 비트 정수를 가지며, 데이터 타입으로 표시됩니다 i128( u128부호없는 정수).

let a: i128 = 170141183460469231731687303715884105727;

Rust는 이러한 i128값을 64 비트 시스템에서 어떻게 작동합니까 ? 예를 들어 어떻게 이것들을 산술합니까?

내가 아는 한 x86-64 CPU의 하나의 레지스터에 값을 맞출 수 없으므로 컴파일러는 어떻게 든 하나의 i128값에 2 개의 레지스터를 사용 합니까? 아니면 그것들을 대신하기 위해 일종의 큰 정수 구조체를 사용하고 있습니까?



답변

모든 Rust의 정수 유형은 LLVM integers 로 컴파일됩니다 . LLVM 추상 시스템은 1에서 2 ^ 23-1 사이의 모든 비트 폭의 정수를 허용합니다. * LLVM 명령어는 일반적으로 모든 크기의 정수에서 작동합니다.

분명히 838307 비트의 아키텍처는 많지 않기 때문에 코드가 네이티브 머신 코드로 컴파일 될 때 LLVM은이를 구현하는 방법을 결정해야합니다. 추상 명령어의 의미는 addLLVM 자체에 의해 정의됩니다. 일반적으로, 네이티브 코드와 동일한 단일 명령어를 갖는 추상 명령어는 해당 네이티브 명령어로 컴파일되며, 그렇지 않은 명령어는 여러 네이티브 명령어로 에뮬레이션되지 않습니다. mcarton의 답변 은 LLVM이 기본 명령어와 에뮬레이트 된 명령어를 모두 컴파일하는 방법을 보여줍니다.

(이것은 네이티브 머신이 지원할 수있는 것보다 큰 정수뿐만 아니라 더 작은 정수에도 적용됩니다. 예를 들어, 현대 아키텍처는 네이티브 8 비트 산술을 지원하지 않을 수 있으므로 add두 개의 명령어 i8가 에뮬레이트 될 수 있습니다 더 넓은 명령으로 여분의 비트는 버려집니다.)

컴파일러는 어떻게 든 하나의 i128값에 2 개의 레지스터를 사용합니까 ? 아니면 그것들을 표현하기 위해 어떤 종류의 큰 정수 구조체를 사용하고 있습니까?

LLVM IR 수준에서 답은 다음 i128과 같습니다 . 다른 모든 단일 값 유형 과 마찬가지로 단일 레지스터에도 맞지 않습니다 . 반면에 일단 머신 코드로 변환 된 후에는 구조체가 정수처럼 레지스터로 분해 될 수 있기 때문에 둘 사이에는 실제로 차이가 없습니다. 그러나 산술을 할 때 LLVM이 모든 것을 두 개의 레지스터에로드하는 것이 안전합니다.


그러나 모든 LLVM 백엔드가 동일한 것은 아닙니다. 이 답변은 x86-64와 관련이 있습니다. 128보다 큰 크기와 2의 비 제곱에 대한 백엔드 지원은 드문 것으로 이해합니다 (Russt가 8, 16, 32, 64 및 128 비트 정수만 노출하는 이유를 부분적으로 설명 할 수 있음). Reddit의 est31에 따르면 rustc은 기본적으로 지원하지 않는 백엔드를 대상으로 할 때 소프트웨어에서 128 비트 정수를 구현합니다.


답변

컴파일러는이를 여러 레지스터에 저장하고 필요한 경우 여러 명령어를 사용하여 해당 값을 산술합니다. 대부분의 ISA에는 x86 과 같은 캐리 추가 기능이 있습니다.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 호출 규칙은 RDX : RAX에서 128 비트 정수를 반환합니다. 그러나 이것은 main 반환 값을 전혀 반환하지는 않습니다. 모든 중복 복사는 순전히 최적화 비활성화에서 비롯되며 Rust는 실제로 디버그 오버플로를 검사합니다. 양식.)

비교를 위해 다음은 x86-64의 Rust 64 비트 정수에 대한 asm입니다. 여기에는 캐리 추가 기능이 필요하지 않으며 각 값에 대해 단일 레지스터 또는 스택 슬롯 만 있습니다.

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

setb / 테스트는 여전히 완전히 중복됩니다. jc 됩니다 (CF = 1이면 점프).

최적화가 활성화되면 Rust 컴파일러는 오버플로를 확인하지 않으므로 다음 +과 같이 작동합니다 .wrapping_add().


답변

예, 32 비트 시스템의 64 비트 정수 또는 16 비트 시스템의 32 비트 정수 또는 8 비트 시스템의 16 비트 및 32 비트 정수와 동일한 방식으로 처리됩니다 (여전히 마이크로 컨트롤러에 적용 가능! ). 예, 숫자를 두 개의 레지스터 나 메모리 위치 또는 실제로 중요하지 않은 곳에 저장합니다. 덧셈과 뺄셈은 두 가지 지침을 취하고 carry 플래그를 사용하는 것이 쉽지 않습니다. 곱셈에는 곱셈과 곱셈이 세 번 필요합니다 (64 비트 칩이 이미 두 개의 레지스터로 출력되는 64×64-> 128 곱하기 연산을 갖는 것이 일반적 임). 나눗셈은 서브 루틴을 필요로하고 매우 느립니다 (일부 상수에 의한 나누기가 교대 또는 곱셈으로 변환 될 수있는 경우는 제외). 그러나 여전히 작동합니다. 비트 및 / 또는 xor는 단순히 상단과 하단에서 별도로 수행해야합니다. 회전 및 마스킹으로 시프트를 수행 할 수 있습니다. 그리고 그것은 거의 모든 것을 다루고 있습니다.


답변

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을 전달합니다. 네 번째 줄은 입력의 상위 단어에 0을 더한 다음 입력 된 경우 1을 더합니다.

이것을 한 자리 숫자를 두 자리 숫자에 간단히 추가 한 것으로 생각할 수 있습니다

  a  b
+ 0  7
______
 

그러나 기초 18,446,744,073,709,551,616에서. 여전히 가장 낮은“숫자”를 먼저 추가하고 다음 열에 1을 넣은 다음 다음 자릿수와 캐리를 추가합니다. 빼기는 매우 비슷합니다.

곱셈은 ​​항등식 (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에서는 부호없는 두 정수의 합이 피연산자 중 하나보다 작은 지 여부를 확인해야합니다. 그렇다면 추가가 오버플로되었습니다. 적어도 조건부 분기없이 캐리 레지스터의 값으로 다른 레지스터를 설정할 수 있습니다.


답변