오버플로우(Overflow)
팩토리얼 값은 숫자가 커질수록 아주 빠르게 증가합니다. 예를 들어, 20의 팩토리얼은 무려 2,432,902,008,176,640,000이나 되죠. 이 숫자는 이미 32비트 정수(i32)가 담을 수 있는 최대값인 2,147,483,647을 훌쩍 넘어버립니다.
산술 연산 결과가 해당 정수 타입이 담을 수 있는 최대값보다 커지는 상황을 **정수 오버플로우(Integer overflow)**라고 부릅니다.
정수 오버플로우는 프로그래밍 규칙을 위반하는 문제이기 때문에 주의해야 합니다. 컴퓨터 입장에서 어떤 정수 타입끼리 연산한 결과는 반드시 그 타입에 맞는 값이어야 하는데, 수학적으로는 맞는 답이 정해진 크기를 초과해서 담을 수 없게 되기 때문입니다.
반대로 결과가 해당 정수 타입의 최소값보다 작아지는 상황은 **정수 언더플로우(Integer underflow)**라고 합니다. 이 섹션에서는 편의상 오버플로우를 위주로 설명하겠지만, 모든 내용은 언더플로우에도 똑같이 적용된다는 사실을 기억해 주세요.
이전에 “변수” 섹션에서 작성했던
speed함수도 사실 일부 입력값에서 언더플로우가 발생할 가능성이 있었습니다. 만약end값이start보다 작다면end - start는 음수가 되어야 하는데, 우리가 썼던u32타입은 음수를 담을 수 없으므로 언더플로우가 발생하게 되죠.
자동 타입 승격은 없습니다
오버플로우 문제에 대응하는 한 가지 방법은 결과를 더 큰 타입으로 자동으로 바꿔주는 것입니다. 예를 들어 u8 타입 두 개를 더했는데 결과가 256(u8::MAX + 1)이 나왔다면, Rust가 알아서 더 큰 타입인 u16으로 결과를 처리해 줄 수도 있겠죠.
하지만 이미 살펴보았듯이, Rust는 타입 변환에 매우 엄격합니다. 따라서 이런 자동 승격은 Rust가 오버플로우 문제를 해결하는 방식이 아닙니다.
어떻게 처리할까요?
자동 승격을 하지 않는다면, 오버플로우가 났을 때 어떤 선택을 할 수 있을까요? 크게 두 가지 방향이 있습니다.
- 연산을 거부하기 (프로그램 중단)
- 어떻게든 정해진 타입 범위 안에서 “합리적인” 값을 내놓기
연산 거부 (패닉)
가장 보수적이고 안전한 방식입니다. 오버플로우가 발생하면 즉시 프로그램을 멈추는 것이죠. 앞서 “패닉” 섹션에서 보았던 패닉(Panic) 메커니즘이 바로 이럴 때 쓰입니다.
“합리적인” 값 내놓기 (래핑)
산술 연산 결과가 최대값을 넘었을 때, 마치 원형 궤도처럼 다시 최소값부터 시작하게 만드는 방식입니다. 이를 **래핑(Wrapping around)**이라고 합니다.
예를 들어, u8 타입에서 1과 255(u8::MAX)를 래핑 덧셈하면 결과는 0(u8::MIN)이 됩니다. 부호 있는 정수도 마찬가지입니다. i8 타입에서 127(i8::MAX)에 1을 더하면 -128(i8::MIN)이 됩니다.
overflow-checks 설정
Rust는 오버플로우 상황에서 어떤 방식으로 동작할지 개발자가 선택할 수 있게 해줍니다. 이 동작은 overflow-checks라는 프로필(Profile) 설정에 의해 결정됩니다.
overflow-checks가true이면, 오버플로우 발생 시 런타임에 패닉을 일으킵니다.overflow-checks가false이면, 오버플로우 발생 시 값을 래핑합니다.
그런데 ’프로필’이 무엇인지 궁금하시죠? 한번 자세히 알아볼까요?
프로필(Profiles)
프로필은 Rust 코드가 컴파일되는 방식을 결정하는 여러 옵션들의 모음입니다.
Cargo에는 크게 4가지 내장 프로필이 있습니다.
dev프로필:cargo build,cargo run,cargo test를 실행할 때 기본적으로 사용됩니다. 로컬 개발을 위한 용도이며, 런타임 성능보다는 컴파일 속도와 풍부한 디버깅 정보를 우선시합니다.release프로필: 실제 배포용(Production) 빌드를 위해 런타임 성능을 극한으로 최적화합니다. 대신 컴파일 시간은 훨씬 오래 걸립니다.--release플래그를 붙여야만 활성화됩니다. (예:cargo build --release)test프로필:cargo test에서 사용되며dev프로필의 설정을 그대로 물려받습니다.bench프로필: 성능 측정용인cargo bench에서 사용되며release프로필 설정을 물려받습니다.
Rust 커뮤니티에는 “릴리스 모드로 빌드하셨나요?“라는 유명한 밈(Meme)이 있습니다. Rust에 입문한 지 얼마 안 된 개발자가 소셜 미디어 등에 “Rust 성능이 왜 이렇게 안 나오죠?“라며 투덜대는데, 알고 보니
--release플래그 없이 빌드했던 상황을 비꼬는 표현이죠.
물론 기본 프로필 외에 나만의 설정을 추가하거나 기존 프로필 내용을 직접 수정할 수도 있습니다.
프로필별 오버플로우 체크 설정
기본적으로 overflow-checks는 다음과 같이 설정되어 있습니다.
dev프로필:truerelease프로필:false
각 프로필의 목적에 맞게 합리적으로 정해진 것이죠. dev는 개발 중에 문제를 빨리 찾을 수 있게 패닉을 일으키고, release는 오버플로우 체크로 인해 속도가 느려지는 것을 방지하기 위해 래핑을 허용합니다.
하지만 두 프로필의 동작이 서로 다르면, 개발할 때는 몰랐던 버그가 실제 서비스 중에 튀어나올 수도 있습니다. 그래서 저희는 가급적 두 프로필 모두에서 overflow-checks를 활성화(True)하는 것을 추천합니다. 잘못된 계산 결과를 조용히 남기는 것보다는, 차라리 프로그램이 죽는 것이 문제 해결에 훨씬 도움이 되기 때문입니다. 오버플로우 체크로 인한 성능 저하는 대부분의 경우 무시할 수 있는 수준이며, 정말 성능이 중요한 부분이라면 직접 벤치마크를 해보고 결정하면 됩니다.
더 읽어보기
- Rust의 정수 오버플로우에 대해 더 깊이 알고 싶다면 “Rust의 정수 오버플로우에 대한 신화와 전설”을 읽어보세요.
Exercise
The exercise for this section is located in 02_basic_calculator/08_overflow