2147483647에 1을 더하면: 언어마다 다른 정수 오버플로
Java int, Go int32, Rust i32에서 2147483647 + 1은 범위를 넘습니다. Java와 Go는 음수, Rust는 기본 Cargo 설정에서 dev 빌드면 패닉, release 빌드면 음수입니다. C의 INT_MAX + 1은 정의되지 않은 동작, Python은 2147483648입니다.
32비트 부호 있는 정수에 담을 수 있는 가장 큰 값은 2,147,483,647입니다. Java의 int, Go의 int32, Rust의 i32가 이 범위를 씁니다. 여기에 1을 더했을 때 프로그램이 기대해도 되는 결과는 언어와 타입의 규칙이 정합니다. 언어가 폭을 정해 두지 않은 타입이라면 구현과 대상 플랫폼도 결과에 영향을 줍니다. 같은 덧셈이 Java에서는 음수가 되고, Rust에서는 빌드 설정에 따라 프로그램이 멈추며, C에서는 결과가 정해져 있지 않습니다.
| 언어와 타입 | 부호 있는 정수가 범위를 넘으면 |
|---|---|
Java int (항상 32비트) |
하위 비트만 남은 값(-2,147,483,648). 예외 없음 |
Go int32 |
하위 비트만 남은 값(-2,147,483,648). 패닉 없음 |
Rust i32 |
오버플로 검사가 켜져 있으면 패닉, 꺼져 있으면 2의 보수로 돌아간 값(-2,147,483,648) |
C int (INT_MAX + 1) |
정의되지 않은 동작. int의 폭은 구현마다 다를 수 있음 |
Python int |
임의 정밀도. 일반 정수 연산에는 고정 폭 오버플로가 없음 |
Java와 Go: 조용히 한 바퀴 돈다
Java의 int는 항상 32비트입니다(JLS 4.2.1). Java 언어 명세(JLS 15.18.2)에 따르면 정수 덧셈이 넘칠 때 결과는 수학적인 합을 충분히 큰 2의 보수 형식으로 나타낸 뒤 남긴 하위 비트입니다. 그래서 결과의 부호가 실제 합의 부호와 다릅니다. JLS 4.2.2에는 정수 연산자가 오버플로를 어떤 방식으로도 알리지 않는다고 적혀 있습니다. 따라서 Integer.MAX_VALUE + 1은 예외 없이 Integer.MIN_VALUE, 즉 -2,147,483,648이 됩니다.
넘친 순간을 잡으려면 Math.addExact를 씁니다. 결과가 int 또는 long 범위를 넘으면 ArithmeticException을 던집니다.
int a = Integer.MAX_VALUE;
int wrapped = a + 1; // -2147483648
int checked = Math.addExact(a, 1); // ArithmeticException
Go 명세의 “Integer overflow” 항목도 비슷합니다. 부호 있는 정수의 +, -, *, /, <<는 넘칠 수 있습니다. 결과는 정수 표현 방식과 연산, 피연산자로 정해지고 런타임 패닉은 일어나지 않습니다. Go의 n비트 정수는 2의 보수로 표현되므로 int32의 최댓값에 1을 더하면 -2,147,483,648이 됩니다.
명세는 컴파일러가 오버플로가 없다고 가정해 최적화해서는 안 된다고도 정합니다. 예를 들어 x < x + 1이 항상 참이라고 가정할 수 없습니다.
var a int32 = math.MaxInt32
b := a + 1 // -2147483648
타입을 꼭 적어야 합니다. Go의 int는 구현에 따라 32비트일 수도 64비트일 수도 있습니다. 64비트 int라면 2147483647 + 1은 넘치지 않고 2,147,483,648이 됩니다.
이 규칙은 실행 중에 계산되는 값에 적용됩니다. Go는 상수식을 정확하게 계산하고, 타입이 있는 상수는 그 타입으로 정확히 표현되어야 합니다. 범위를 벗어나는 타입 있는 상수는 컴파일 오류가 됩니다.
Rust: 빌드 설정에 따라 다르다
Rust 레퍼런스는 산술 오버플로를 프로그래머의 오류로 봅니다. debug_assert!가 켜진 빌드에서는 구현이 오버플로를 검사해 패닉을 일으켜야 합니다. 그 밖의 빌드에서는 패닉을 낼지 조용히 돌아간 값을 낼지 구현이 고를 수 있습니다. 돌아간 값을 낼 때는 2의 보수 규칙을 따라야 합니다.
언어 규칙과 별개로 실제 기본값은 Cargo 프로필이 정합니다. Cargo의 기본 dev 프로필은 overflow-checks = true이고, 기본 release 프로필은 overflow-checks = false입니다. 두 값 모두 프로젝트에서 바꿀 수 있습니다. 기본 프로필을 그대로 쓰면 i32::MAX + 1은 개발 빌드에서 패닉을 일으키고 release 빌드에서는 -2,147,483,648을 돌려줍니다.
어떤 동작을 원하는지는 메서드 이름으로 코드에 남길 수 있습니다.
let a = i32::MAX;
a.checked_add(1); // None
a.wrapping_add(1); // i32::MIN
a.saturating_add(1); // i32::MAX
C: 부호 있는 정수에는 약속이 없다
C 표준은 int가 32비트라고 정하지 않습니다. 그래서 C에서는 INT_MAX + 1이라는 식으로 생각하는 편이 정확합니다. 2,147,483,647은 int의 최댓값이 그 값인 구현에서만 INT_MAX입니다.
C23 작업 초안 N3220의 6.5.1에 따르면 식의 결과가 그 타입으로 표현할 수 있는 범위를 벗어나면 동작이 정의되지 않습니다. 정의되지 않은 동작이란 표준이 아무것도 요구하지 않는 동작입니다. INT_MAX + 1이 INT_MIN이 된다는 보장도, 프로그램이 멈춘다는 보장도 없습니다.
부호 없는 정수는 다릅니다. 6.2.5에 따르면 부호 없는 연산은 2^N을 법으로 계산되므로 오버플로가 생기지 않습니다. UINT_MAX + 1u는 0입니다.
C에서 넘침을 검사하려면 더하기 전에 범위를 비교합니다. b > 0일 때 a > INT_MAX - b이면 a + b는 넘칩니다. GCC의 __builtin_add_overflow를 쓸 수도 있습니다.
int r;
if (__builtin_add_overflow(INT_MAX, 1, &r)) {
/* 넘침 처리 */
}
GCC 문서에 따르면 이 함수는 두 피연산자를 무한 정밀도로 더하고, 결과를 세 번째 인수가 가리키는 변수에 저장한 뒤, 저장된 값이 무한 정밀도 결과와 다르면 true를 반환합니다.
Python: 임의 정밀도 정수
Python 문서에 따르면 int의 정밀도에는 제한이 없습니다. 일반적인 정수 덧셈은 고정 폭 경계에서 넘치지 않으며, 2147483647 + 1은 2147483648이 됩니다.
다만 값을 고정 폭 정수로 변환하거나 고정 폭 값을 요구하는 인터페이스에 넘길 때는 범위를 따로 확인해야 합니다.
32비트 경계가 실제로 보이는 곳
2,147,483,647은 16진수로 7fffffff이고, 1을 더한 2,147,483,648은 80000000입니다. 7fffffff에 1을 더해 80000000이 되면 최상위 비트가 1이 되고 나머지 하위 31비트는 모두 0이 됩니다. 32비트 2의 보수에서는 바로 이 비트 패턴이 최솟값 -2,147,483,648입니다. 진법 변환기에 두 수를 넣어 16진수 값을 확인할 수 있습니다.
부호 있는 32비트 유닉스 시간에 관한 내용은 유닉스 시간과 2038년 문제에서 이어집니다. 나눗셈에서 언어마다 결과가 갈리는 사례는 음수 나머지 연산에서 다뤘습니다.
다른 언어에서 합계나 카운터 계산을 옮긴다면 먼저 값의 타입과 폭을 확인해야 합니다. 그다음 오버플로를 오류로 볼지, 돌아간 값을 받아들일지 정하고 Math.addExact, checked_add, wrapping_add처럼 그 선택이 이름에 드러나는 함수를 쓰면 됩니다.
참고한 명세와 문서
- JLS SE 21 §4.2
- JLS SE 21 §15.18.2
- Java
Math.addExact - Go specification: Integer overflow
- Go specification: Numeric types
- Go specification: Constant expressions
- Rust Reference: Overflow
- Rust Reference: Behavior not considered
unsafe - Cargo Book: Profiles
- Rust
i32 - C23 작업 초안 N3220
- GCC: Integer Overflow Builtins
- Python 표준 라이브러리: Numeric Types