목차
부동소수점의 MIN_VALUE는 엄청나게 작은 숫자가 아니다
코딩을 공부한 사람이라면 누구나 한 번쯤, 배열의 최댓값(최솟값)을 찾는 문제를 풀어본 적이 있을겁니다. 그런게 몇 시간의 발목을 잡게될 줄 누가 알았을까요...
2017-07-22#프로그래밍
💡 이 글은 작성된지 1년 이상 지났습니다. 정보글의 경우 최신 내용이 아닐 수 있음에 유의해주세요.
코딩을 공부한 사람이라면 누구나 한 번쯤, 배열의 최댓값(최솟값)을 찾는 문제를 풀어본 적이 있을겁니다.
보통은 다음과 같은 풀이를 배웁니다.
그리고 이를 코드로 옮기면 다음과 같습니다.
#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 5> arr = {5, 1, 7, 9, 3};
int max = -1;
for (int i = 0; i < arr.size(); i++) {
if (arr[i] > max) {
max = arr[i];
}
}
cout << max << endl; // max == 9
}
위에서는 '충분히 작은 숫자'로 -1을 대입했지만, 이는 배열에 -1보다 작은 음수만 들어있는 경우를 처리하지 못합니다.
따라서 어떤 상황에서도 통용할 수 있는 '충분히 작은 숫자'를 표현하기 위해서는 다음과 같은 방식을 사용하는게 좋습니다.
int max = 0x80000000
// 또는...
int max = INT_MIN
(c++ 기준) 두 코드 모두 int
가 표현할 수 있는 가장 작은 숫자를 나타냅니다. INT_MIN
은 0x80000000의 매크로이므로, 두 표현은 사실상 동일합니다.
똑같은 방식으로 실수 배열의 최댓값을 구하는 코드를 짜보겠습니다.
int main() {
array<float, 5> arr = {-5.2, -1.8, -7.4, -9.6, -3.0};
float max = FLT_MIN;
for (int i = 0; i < arr.size(); i++) {
if (arr[i] > max) {
max = arr[i];
}
}
cout << max << endl;
}
int
를 float
로, INT_MIN
을 FLT_MIN
으로 변경했습니다. 당연히 기대하는 최댓값은 -1.8입니다. 하지만 위 코드를 실제로 실행해보면...
응, 아니야
이해할 수 없는 실수가 하나 튀어나옵니다. 배열에 있는 숫자도 아닐 뿐더러, 양수니까 다른 값들보다 작지도 않은데요, 왜 이런 결과가 나왔을까요?
int main() {
cout << FLT_MIN << endl;
}
1.17549e-38의 정체는 FLT_MIN
이었습니다. 당연히 FLT_MIN
은 float의 최솟값이라고 생각했었는데요, 왜 저런 애매한 값이 설정되어있을까요?
레퍼런스 문서(참고 링크)를 찾아보면, FLT_MIN
은 'Minimum representable positive floating-point number.' 라고 써있습니다. 한국말로 옮기자면 '부동 소수점으로 표현 가능한 가장 작은 양수' 쯤 되겠네요. 즉, 0보다 큰 수 중 가장 작은 수를 의미합니다.
왜 이런 애매한 표현을 굳이 FLT_MIN
이라는 매크로로 정의해놓았을까요?
float은 부동 소수점(floating point)이라는 단어에서 온 표현입니다. 단어 그대로 소수점이 떠다닌다는(floating) 뜻인데요, 예를 들어 3.141592
라는 숫자는 그 값 자체가 아닌, +
부호, 3141592
의 가수와 10^(-6)
의 지수의 세 가지로 나누어 저장합니다.
숫자를 저장하는 방식에 대한 표준으로는 IEEE 754가 있습니다. 예를 들어 -118.625
라는 값을 IEEE 754 32비트 부동 소수점으로 저장한다면 다음과 같습니다.
출처: 위키미디어 공용 (CC BY-SA 3.0)
이러한 방식의 문제는, 실수의 정확한 값이 아닌 근삿값이 저장된다는 점입니다. 알고리즘 문제를 풀어보신 분들은 근삿값으로 인한 연산의 오차가 생각보다 빈번하게, 사실상 무조건 발생한다는 점을 이미 알고 계실겁니다.
일상적인 연산에서는 약간의 오차는 눈감아줄만 하지만, 때로는 값이 정밀도를 따지기 위해 float이 표현할 수 있는 가장 미세한 값을 알 필요가 있습니다. FLT_MIN
은 바로 그런 니즈를 위해 탄생한 매크로입니다.
다시금 정의를 되새겨 보자면, FLT_MIN
은 부동 소수점으로 표현 가능한 가장 작은 양수입니다. 이 말은 곧 0보다는 크지만 0에서 가장 가까운, 즉 float
으로 표현 가능한 가장 '미세한' 값이라는 뜻이 됩니다. 1.17549e-38라는 값은 float
이 표현 가능한 가장 미세한 값인 셈입니다.
반대로 FLT_MAX
는 float
이 표현 가능한 가장 거대한 수, 값으로는 3.40282e+38을 나타냅니다.
그렇다면 float
으로 표현할 수 있는 가장 값이 작은 숫자는 어떻게 표현할 수 있을까요?
앞서 부동 소수점의 정의를 제대로 이해하셨다면 답은 의외로 간단한데요, 바로 -FLT_MAX
입니다.
부동 소수점은 int
등과는 달리 부호 그 자체에 하나의 비트가 할당되어있어, 모든 숫자의 음/양을 대칭으로 표현할 수 있습니다. 지수 비트와 가수 비트가 동일하고 부호 비트만 다른 두 수는, 쉽게 풀자면 '절댓값은 같고 부호가 다른 두 수'를 표현하기 때문입니다.
따라서 FLT_MAX
가 float
이 표현 가능한 가장 큰 수(== 0에서 거리가 가장 먼 양수)였으므로, 여기에 -1을 곱해 부호 비트를 반전시키면 0에서 거리가 가장 먼 음수, 즉 가장 작은 수가 됩니다.
이제 배열의 최댓값을 구하는 코드를 다시 짜보겠습니다.
int main() {
array<float, 5> arr = {-5.2, -1.8, -7.4, -9.6, -3.0};
float max = -FLT_MAX;
for (int i = 0; i < arr.size(); i++) {
if (arr[i] > max) {
max = arr[i];
}
}
cout << max << endl;
}
코드를 실행해보면, 이제는 원하는 값인 -1.8이 제대로 출력됩니다.
하필이면 정의를 FLT_MIN
라는 헷갈리는 명칭으로 정해서 꽤나 혼동을 유발하는데요, 다른 언어에서는 어떨까요?
우선 Java에서 java.lang.Float.MIN_NORMAL
정의는 다음과 같습니다.
A constant holding the smallest positive normal value of type float, 2^(-126)
Python에서는 sys.float_info.min
이라는 값으로 저장되어있습니다.
minimum positive normalized float (DBL_MIN of the C99 standard)
아무래도 info라는 단어가 끼어있어 헷갈림이 조금 덜하지만, 여전히 같은 내용을 표현하고 있습니다. 그 밖에도 확인해본 모든 언어가 부동 소수점의 MIN
을 가장 작은 양수를 표현하는 매크로로 사용하고 있었습니다.
.
.
.
조금의 변명을 해보자면, Stackoverflow의 영어 네이티브들도 굉장히 헷갈린다는 듯 합니다. 처음 정의한 사람이 좀 더 명확한 단어를 사용했으면 좋았겠다는 생각이 드네요.
이 내용은, 아주 단순한 로직을 짜던 중 도저히 이해할 수 없는 버그가 발생해 이를 디버깅하면서 시작되었습니다. 대략 시간 단위로 날려먹은 것 같네요. 이 글을 통해 대한민국 개발자들의 삽질이 조금이라도 줄어들수 있으면 좋겠습니다.
관련된 글
Rails와 GitHub Actions에 커버리지 레포트를 달아보자
이 블로그의 CMS이기도 한 Shiori를 대폭 리팩토링하면서 테스트가 얼마나 잘 작성되어있는지 궁금해졌습니다.
Rails Global ID로 전역 객체 식별하기
Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다.
Ruby on WebAssembly: 살짝 맛보기
Ruby 3.2에 추가된 WebAssembly 지원을 간단하게 테스트해봅시다.
본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.
© 2011 - 2024 Hoerin Doh, All rights reserved.
작성한 댓글은 giscus를 통해 GitHub Discussion에 저장됩니다.