lynlab logo

blog

about

#development

부동소수점의 MIN_VALUE는 엄청나게 작은 숫자가 아니다

코딩을 공부한 사람이라면 누구나 한 번쯤, 배열의 최댓값(최솟값)을 찾는 문제를 풀어본 적이 있을겁니다. 그런게 몇 시간의 발목을 잡게될 줄 누가 알았을까요...

profile picture

Hoerin Doh | 2017. 07. 22

코딩을 공부한 사람이라면 누구나 한 번쯤, 배열의 최댓값(최솟값)을 찾는 문제를 풀어본 적이 있을겁니다.

처음 배우는 풀이

보통은 다음과 같은 풀이를 배웁니다.

  1. '충분히 작은' 어떤 숫자를 x라고 한다.
  2. 배열의 첫 번째 값이 x보다 크다면, x를 그 값으로 바꾼다.
  3. 배열의 나머지 값들에 대해서 2번 과정을 반복한다.
  4. 모든 값들의 비교가 끝나면 x는 그 배열의 최댓값이 된다.

그리고 이를 코드로 옮기면 다음과 같습니다.

#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;
}

intfloat로, INT_MINFLT_MIN으로 변경했습니다. 당연히 기대하는 최댓값은 -1.8입니다. 하지만 위 코드를 실제로 실행해보면...

1.17549e-38
응, 아니야

이해할 수 없는 실수가 하나 튀어나옵니다. 배열에 있는 숫자도 아닐 뿐더러, 양수니까 다른 값들보다 작지도 않은데요, 왜 이런 결과가 나왔을까요?

레퍼런스를 찾아보면...

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의 정의

float은 부동 소수점(floating point)이라는 단어에서 온 표현입니다. 단어 그대로 소수점이 떠다닌다는(floating) 뜻인데요, 예를 들어 3.141592라는 숫자는 그 값 자체가 아닌, + 부호, 3141592의 가수와 10^(-6)의 지수의 세 가지로 나누어 저장합니다.

숫자를 저장하는 방식에 대한 표준으로는 IEEE 754가 있습니다. 예를 들어 -118.625라는 값을 IEEE 754 32비트 부동 소수점으로 저장한다면 다음과 같습니다.

이미지
출처: 위키미디어 공용 (CC BY-SA 3.0)

이러한 방식의 문제는, 실수의 정확한 값이 아닌 근삿값이 저장된다는 점입니다. 알고리즘 문제를 풀어보신 분들은 근삿값으로 인한 연산의 오차가 생각보다 빈번하게, 사실상 무조건 발생한다는 점을 이미 알고 계실겁니다.

그래서 FLT_MIN이 나왔다

일상적인 연산에서는 약간의 오차는 눈감아줄만 하지만, 때로는 값이 정밀도를 따지기 위해 float이 표현할 수 있는 가장 미세한 값을 알 필요가 있습니다. FLT_MIN은 바로 그런 니즈를 위해 탄생한 매크로입니다.

다시금 정의를 되새겨 보자면, FLT_MIN은 부동 소수점으로 표현 가능한 가장 작은 양수입니다. 이 말은 곧 0보다는 크지만 0에서 가장 가까운, 즉 float으로 표현 가능한 가장 '미세한' 값이라는 뜻이 됩니다. 1.17549e-38라는 값은 float이 표현 가능한 가장 미세한 값인 셈입니다.

반대로 FLT_MAXfloat이 표현 가능한 가장 거대한 수, 값으로는 3.40282e+38을 나타냅니다.

그러면 실제로 float의 가장 작은 숫자는?

그렇다면 float으로 표현할 수 있는 가장 값이 작은 숫자는 어떻게 표현할 수 있을까요?

앞서 부동 소수점의 정의를 제대로 이해하셨다면 답은 의외로 간단한데요, 바로 -FLT_MAX 입니다.

부동 소수점은 int 등과는 달리 부호 그 자체에 하나의 비트가 할당되어있어, 모든 숫자의 음/양을 대칭으로 표현할 수 있습니다. 지수 비트와 가수 비트가 동일하고 부호 비트만 다른 두 수는, 쉽게 풀자면 '절댓값은 같고 부호가 다른 두 수'를 표현하기 때문입니다.

따라서 FLT_MAXfloat이 표현 가능한 가장 큰 수(== 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의 영어 네이티브들도 굉장히 헷갈린다는 듯 합니다. 처음 정의한 사람이 좀 더 명확한 단어를 사용했으면 좋았겠다는 생각이 드네요.

이 내용은, 아주 단순한 로직을 짜던 중 도저히 이해할 수 없는 버그가 발생해 이를 디버깅하면서 시작되었습니다. 대략 시간 단위로 날려먹은 것 같네요. 이 글을 통해 대한민국 개발자들의 삽질이 조금이라도 줄어들수 있으면 좋겠습니다.

관련 포스트

asdf — 하나의 명령어로 관리하는 버전 매니저 썸네일

asdf — 하나의 명령어로 관리하는 버전 매니저

요즘 세상에 하나의 언어로 모든 시스템을 개발하는 경우는 보기 드뭅니다. asdf는 각각의 개발 환경을 플러그인 형식으로 만들어 하나의 명령어로 관리하기 위해 탄생했습니다.

Keybase와 GPG 키를 이용해 Git 커밋에 서명하기 썸네일

Keybase와 GPG 키를 이용해 Git 커밋에 서명하기

Git은 커밋한 사람의 이메일 주소를 검증하지 않습니다. 하지만 GPG 키를 이용해 커밋에 서명을 남기면 커밋한 사람이 본인임을 증명할 수 있습니다.

[Drone] 인메모리 볼륨을 활용해 CI 속도 향상하기 썸네일

[Drone] 인메모리 볼륨을 활용해 CI 속도 향상하기

데이터 IO 작업으로 인해 CI/CD 프로세스에 병목이 생긴다면, 인메모리 볼륨을 활용해 속도를 크게 향상시킬 수 있습니다.

크리에이티브 커먼즈 라이선스

본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0  국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2021 Hoerin Doh, All rights reserved.