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


128

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

let a: i128 = 170141183460469231731687303715884105727;

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

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



54
손가락이 10 개일 때 두 자리 정수는 어떻게 작동합니까?
Jörg W Mittag

27
@JorgWMittag : Ah-오래된 "단 손가락이 10 개인 두 자리 숫자"ploy. ㅎㅎ 그 늙은 사람으로 나를 속일 수 있다고 생각 했어? 글쎄, 내 친구, 어떤 2 ​​학년이 당신에게 말할 수 있듯이-그것은 발가락이 무엇입니까! ( Peter Sellers에게 그리고 사과 Lytton에게 미안한 사과와 더불어 :-)
Bob Jarvis-모니카

1
FWIW 대부분의 x86 컴퓨터에는 SIMD 작업을위한 특수한 128 비트 이상의 레지스터가 있습니다. en.wikipedia.org/wiki/Streaming_SIMD_Extensions 편집 참조 : @eckes의 의견을 놓쳤습니다
Ryan1729

4
@ JörgWMittag Nah, 컴퓨터 과학자들은 개별 손가락을 줄이거 나 늘림으로써 이진수로 계산합니다. 그리고 지금, 132 명, 나는 집에 간다 ;-D
Marco13

답변:


141

모든 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 비트 정수를 구현합니다.


1
허, 나는 왜 더 일반적인 2 ^ 32 대신 2 ^ 23인지 궁금합니다 (컴파일러 백엔드가 지원하는 정수의 최대 비트 폭이 아니라 숫자가 얼마나 자주 나타나는지에 대해 광범위하게 말하면 ...)
Fund 모니카의 소송

26
@NicHartley LLVM의 기본 클래스 중 일부에는 하위 클래스가 데이터를 저장할 수있는 필드가 있습니다. 들어 Type클래스이 수단은 어떤 유형이 무엇 저장소에 8 비트 (기능 블록의 정수, ...) 및 서브 데이터의 24 비트가있다. IntegerType클래스 인스턴스는 깔끔하게 32 비트에 적합 할 수 있도록 크기를 저장하기 위해 이들 24 비트를 사용!
Todd Sewell

56

컴파일러는이를 여러 레지스터에 저장하고 필요한 경우 여러 명령어를 사용하여 해당 값을 산술합니다. 대부분의 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().


4
@Anush No, rax / rsp / ...는 64 비트 레지스터입니다. 각 128 비트 숫자는 두 개의 레지스터 / 메모리 위치에 저장되므로 두 개의 64 비트가 추가됩니다.
ManfP

5
@Anush : 아니요. 최적화가 비활성화 된 상태로 컴파일 되었기 때문에 너무 많은 명령어를 사용하고 있습니다. 컴파일러가 수행을 중지하도록 최적화를 비활성화하는 대신 두 개의 args를 사용하고 값 (이 godbolt.org/z/6JBza0 과 같은)을 반환하는 함수를 컴파일하면 add / adc와 같은 훨씬 간단한 코드가 표시 됩니다. 컴파일 타임 상수 인수에 대한 상수 전파. u128
Peter Cordes

3
@ CAD97 릴리스 모드 래핑 산술을 사용 하지만 디버그 모드와 같이 오버플로 및 패닉을 확인하지 않습니다. 이 동작은 RFC 560에 의해 정의되었습니다 . UB가 아닙니다.
trentcl

3
@PeterCordes : 특히 Rust 언어는 오버플로가 지정되지 않았으며 rustc (유일한 컴파일러)는 Panic 또는 Wrap의 두 가지 동작을 지정합니다. 이상적으로는 기본적으로 Panic이 사용됩니다. 실제로, 최적화되지 않은 코드 생성으로 인해 릴리스 모드에서 기본값은 줄 바꿈이며 장기 목표는 코드 생성이 주류 사용에 "충분히"좋은 경우 패닉으로 전환하는 것입니다. 또한 모든 Rust 정수 유형은 동작을 선택하기위한 명명 된 작업 (checked, wrap, saturating)을 지원하므로 작업별로 선택한 동작을 재정의 할 수 있습니다.
Matthieu M.

1
@MatthieuM .: 네, 래핑 vs. 체크 vs. 추가 / 서브 / 시프트 / 프리미티브 유형의 모든 메소드를 좋아합니다. C의 unsigned를 감싸는 것보다 훨씬 낫습니다 .UB는 서명을 강제로 선택했습니다. 어쨌든 일부 ISA는 Panic에 대한 효율적인 지원을 제공 할 수 있습니다. (0 또는 1로 덮어 쓴 x86의 OF 또는 CF와 달리) Agner Fog의 제안 된 ForwardCom ISA ( agner.org/optimize/blog/read.php?i=421#478 ) 그러나 여전히 계산을 수행하지 않도록 최적화를 제한합니다. 녹 소스는하지 않았다. : /
Peter Cordes

30

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


26

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


1
마지막 단락 : 결과 의 높은 비트를보고 부호없는 두 숫자 중 큰 숫자 를 감지하려면 비트 입력에 대한 비트 하위 결과 sub가 필요합니다 . 즉, 동일한 너비 결과의 부호 비트가 아닌 캐리 아웃을 확인해야합니다. 이것이 x86 부호없는 분기 조건이 SF (비트 63 또는 31)가 아닌 CF (전체 논리 결과의 비트 64 또는 32)를 기반으로하는 이유입니다. n+1n
Peter Cordes

1
re : divmod : AArch64의 접근 방식은 나누기와 정수를 수행하는 명령을 제공 x - (a*b)하여 피제수, 몫 및 제수에서 나머지를 계산하는 것입니다. (나눗셈 부분에 곱하기 역수를 사용하는 상수 제수에도 유용합니다). div + mod 명령을 단일 divmod 작업에 통합하는 ISA에 대해서는 읽지 못했습니다. 그거 멋지다.
Peter Cordes

1
re : flags : 예, 플래그 출력은 OoO exec + register-renaming이 어떻게 든 처리해야하는 두 번째 출력입니다. x86 CPU는 FLAGS 값이 기반으로하는 정수 결과로 몇 개의 추가 비트를 유지하여이를 처리하므로 필요할 때 ZF, SF 및 PF가 즉시 생성 될 수 있습니다. 이것에 대한 인텔 특허가 있다고 생각합니다. 따라서 별도로 추적해야하는 출력 수가 1로 줄어 듭니다. (Intel CPU에서는 uop가 정수 레지스터를 둘 이상 쓸 수 없습니다 mul r64.
Peter Cordes

1
그러나 효율적인 확장 정밀도를 위해서는 플래그가 매우 좋습니다. 주요 문제는 수퍼 스칼라 순서대로 실행하기 위해 레지스터 이름을 바꾸지 않는 것 입니다. 플래그는 WAW 위험 (쓰기 후 쓰기)입니다. 물론 캐리에 추가 명령은 3 입력이므로 추적해야 할 중요한 문제이기도합니다. 브로드 웰 디코딩 인텔 전에 adc, sbb그리고 cmov2 마이크로 연산 각각. (하 스웰은 FMA 3 입력 마이크로 연산이, 브로드 웰은 정수로 그 확장 소개했다.)
피터 코르

1
플래그가있는 RISC ISA는 일반적으로 플래그 설정을 선택적으로하며 추가 비트에 의해 제어됩니다. 예를 들어 ARM과 SPARC는 이와 같습니다. 평상시와 같이 PowerPC는 모든 것을 더욱 복잡하게 만듭니다. 8 개의 조건 코드 레지스터 (저장 / 복원을 위해 하나의 32 비트 레지스터로 묶음)가 있으므로 cc0 또는 cc7 등으로 비교할 수 있습니다. 그리고 AND 또는 OR 조건 코드가 함께! 분기 및 cmov 명령어는 읽을 CR 레지스터를 선택할 수 있습니다. 따라서 x86 ADCX / ADOX와 같이 한 번에 여러 플래그 뎁 체인을 사용할 수 있습니다. alanclements.org/power%20pc.html
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.