컴퓨터가 실수를 저장하는 방식. 그리고 0.1 + 0.1 + 0.10.3과 같지 않은 이유. 이건 언어의 버그가 아니라 저장 방식에서 필연적으로 따라오는 성질이라, 자바든 자바스크립트든 똑같이 일어난다.

IEEE 754의 세 필드

IEEE 754 표준을 따르며, 실수 하나를 세 칸으로 쪼개 담는다.

필드float (32비트)double (64비트)무엇
부호부1비트1비트양수 0, 음수 1
지수부8비트11비트소수점을 얼마나 옮겼는지
가수부23비트52비트유효숫자

5.125를 담는다면 먼저 2진법으로 바꿔 101.001을 얻고, 점을 맨 왼쪽으로 옮겨 1.01001 × 2²로 만든다. 점 뒤의 01001을 가수부에 밀어넣는데, 맨 앞의 1은 항상 1이라 저장하지 않는다. 마지막으로 지수 2에 127을 더해 지수부에 넣는다.

127을 더하는 이유는 음수 지수도 담아야 하기 때문이다. 0.001 같은 수는 지수가 음수인데, 부호 비트를 따로 두지 않고 전부 127만큼 밀어놓으면 양수만으로 표현할 수 있다. 이 127을 bias라고 하고, 지수부가 k비트일 때 2^(k-1) - 1이다.

오차가 생기는 이유

오차는 가수부가 유한해서 생긴다.

10진수 0.125는 2진수로 0.001이라 깔끔하게 떨어진다. 그런데 0.1은 2진수로 0.00011001100110011...이 무한히 반복하는 순환소수다. 무한한 것을 유한한 칸에 넣을 수 없으니 float은 앞 23비트, double은 앞 52비트까지만 잘라서 저장한다. 그래서 저장된 값은 0.1이 아니라 0.1에 아주 가까운 다른 수다. 그걸 세 번 더하면 0.3에 가까운 다른 수가 나온다.

비교할 때의 허용 오차

오차를 없앨 수는 없지만 크기는 정해져 있다. float은 유효숫자 6자리로 상대오차 10⁻⁶까지, double은 유효숫자 15자리로 상대오차 10⁻¹⁵까지 안전하다. 그래서 실수를 다루는 문제는 절대오차나 상대오차를 얼마까지 허용한다는 단서를 준다. 정확히 같은지 비교하지 말고 차이가 허용치 안인지를 보라는 뜻이다. 두 실수가 같은지 알고 싶으면 차이가 대략 10⁻¹² 이하면 같다고 처리하는 것이 안전하다.

double에 long 범위의 값을 담아서도 안 된다. double은 유효숫자 15자리인데 long은 최대 19자리라 10¹⁸과 10¹⁸ + 1을 구분하지 못한다. int는 담아도 오차가 생기지 않는다.

패트리어트 요격 실패

이 사소한 오차로 사람이 죽은 적도 있다. 걸프전 당시 미군의 패트리어트 요격 시스템은 시간을 0.1초 단위로 셌고, 숫자 하나에 24칸을 썼다. 단정도보다도 가수부가 짧으니 오차가 더 컸다. 시간당 0.0034초씩 어긋났는데, 100시간을 연속 가동하니 0.34초가 되었고, 초속 1500m로 날아오는 미사일 앞에서 0.34초는 500m다. 요격에 실패했고 28명이 전사했다.

오차를 피하는 방법

피하는 방법은 셋이다. 정확해야 하는 값은 정수로 저장한다. 돈이 대표적이라 5.1달러를 float로 담지 말고 5100센트로 담고, 자바라면 BigDecimal도 방법이다. 굳이 실수를 써야 하면 반올림을 끼운다.

precision = Math.pow(10, precision)
Math.ceil(num * precision) / precision

정밀도가 필요하면 double을 쓴다. 가수부가 52비트로 늘어나 오차가 훨씬 작아지는 대신 메모리를 두 배 쓴다.

참고

bias는 지수부가 k비트일 때 2^(k-1) - 1이다. 원본은 2^(k-1)이라고 적었는데, 그러면 k가 8일 때 128이 되어 원본이 같은 줄에서 쓴 127과 어긋난다. Wikipedia - Exponent bias

관련

출처