출처: https://www.truenorthfloatingpoint.com/problem
1991년 걸프 전쟁 때, 패트리어트 미사일 방어 시스템이 스커드 미사일을 요격하지 못해 28명이 사망하고 100명이 부상하는 큰 사고가 발생한다. 이는 100시간동안 누적된 float 오차가 원인으로 파악되었으며, 0.34초까지 늘어난 시간 오차 때문에 미사일을 정확히 요격하지 못했다. float 때문에 발생한 인명 사고는 이 뿐만이 아니다. 어쩌면 내 손에 의해 이러한 사고가 발생할 수도 있는 노릇이다. 때문에 나는 이를 교훈 삼아 float를 이해해야만 한다.
원인은?
인명사고를 일으킨 float의 결함은 과연 어디서 비롯한 걸까? 그 해답은 float 구조 (IEEE 754)를 파해치면 알 수 있다.
float의 메모리 구조는 위 그림처럼 생겼고, 값을 저장하는 과정은 다음과 같다.
- 10진수 소수를 2진수 소수로 변환
- e.g. 6.1 > 110.0001100110011...
- 정규화
- 정수 부분을 한 자리만 남기고 그것이 1이도록 한다.
- e.g. 1.10000110011... * 2^2
- 지수는 지수부 메모리에, 소숫점 아래(mentissa)는 가수부 메모리에 적재한다.
- 1.100001100... * 2^2 (초록색이 가수부, 파랑색이 지수부)
- 이 때, 소숫점 아래가 순환 소수 이거나 23자리를 넘어가면 저장할 수 있는 만큼만 저장하고 나머지는 없앤다.
십진수를 이진수로 변환하는 과정에서 순환소수가 되거나 가수부의 23비트를 초과하는 경우가 발생할 수 있다는 점이 중요하다. 이때 초과하는 비트들은 버림(rounding) 처리되며, 이로 인해 정확한 값이 아닌 근사값이 메모리에 저장된다. 예를 들어, 십진수 0.1은 이진수로 변환 시 무한 반복 소수가 되며, 이를 float 타입(단정도, 32비트)으로 저장하면 약 **0.10000000149011612...**와 같은 근사값으로 표현된다.
이러한 부동소수점 오차가 누적되면서 발생한 대표적인 사고가 바로 패트리어트 미사일 오작동 사건이다. 이 비극적인 사고는 수치 오차에 대한 이해 부족이 얼마나 치명적인 결과를 초래할 수 있는지를 보여주는 사례다.
대책은?
- 더 큰 자료형
- 32비트 크기의 float 대신, 64비트 크기의 double을 사용하면 늘어난 비트만큼 오차가 줄어든다.
- 0.1을 저장할 때, float는 **0.10000000149011612...** double은 **0.10000000000000000555...**로 저장된다.
- double의 정밀도가 float보다 무려 9가지라 높다.
- 정수 기반 연산
- int 같은 정수형 데이터를 사용해 오차를 피한다.
- e.g. 1.00 달러 > 100 센트
- 오차 허용 비교 ( Tolerance / Epsilon )
- 실수끼리 비교할 때 어느 정도의 오차는 허용한다.
- e.g. (0.1f + 0.2f) - 0.3f 이 오차 때문에 정확히 0은 아니지만 0인 걸로 친다.맘
마무리
개발자는 자신이 다루는 시스템의 내부 동작 원리를 깊이 이해하고 통제할 수 있어야 한다. 근데 float 자료형 하나만 잘못 써도 대형 사고가 발생하는데, 나머지는 어떡하지...? 벽이 느껴지지만, 그저 나아가야겠지. 괴물을 다루려는 자, 괴물이 되어라!!!
'그 외 > 클래식' 카테고리의 다른 글
C#, C++ 1,000,000번 연산 속도는 왜 다를까 (0) | 2024.08.22 |
---|---|
60FPS를 만들기 위한 조건 (0) | 2024.08.21 |
시간복잡도 (0) | 2024.03.02 |
헝가리안 표기법 (0) | 2024.02.14 |