동등성 판별시 부동소수점 오차
1
System.out.println(0.1 + 0.2 == 0.3); // false
위의 코드는 놀랍게도 false를 출력합니다. 이는 부동소수점 오차로 인해 발생하는 문제입니다. 컴퓨터에서 소수점을 표현하는 방식으로 2가지가 있습니다. 고정 소수점 방식과 부동 소수점 방식입니다.
고정 소수점 방식은 부호, 정수부, 소수부를 활용해 소수를 표현합니다. 직관적인 표현 방식이죠. 예를 들어 12.345를 표현한다면 부호에는 양수를, 12는 정수부에, 345를 소수부에 표현합니다. 이 방식은 정밀도가 매우 높지만, 표현할 수 있는 수의 범위가 너무 좁다는 한계가 있습니다.
부동 소수점 방식은 부호, 가수부, 지수부로 소수를 표현합니다.
\[\pm(1.가수부)\times2^{지수부-127}\]이 방식을 통해 고정 소수점 방식에 비해 훨씬 넓은 범위의 수를 표현할 수 있습니다. 하지만 2의 지수를 곱해 표현하므로 10진수를 정확하게 표현하는데에는 한계가 있습니다. 그래서 오차가 발생할 수 밖에 없고, 처음에 언급했던 0.1+0.2는 출력해보면 0.30000000000000004가 나옵니다. (JVM에 따라 다를 수 있습니다.)
이를 해결하기 위해서 어느정도의 오차를 허용하는 방식으로 동등성 비교를 할 수 있습니다.
1
if(Math.abs(a - b) < epsilon)
이렇게 a와 b의 차이를 계산해서 어느정도의 오차는 무시하는 방식으로 조건문을 구성할 수 있습니다. epsilon은 허용할 오차의 범위를 말합니다. 보통 매우 작은 숫자 0.00000001 정도로 설정하는 듯 합니다.
대소 비교시 NaN과 -0.0f 문제
Double과 Float을 비교할 때 NaN과 -0.0f도 고려해주어야 합니다. 단순 비교(<, > 등)시에는 큰 문제가 되진 않지만, 비트로 변환해서 비교할 때는 NaN를 잘 다루어야 합니다. NaN은 지정된 비트가 있기 때문입니다. 참고로 x=NaN이면 x != x는 항상 true가 됩니다. 이를 활용해서 NaN임을 판별할 수 있습니다.
위에서 언급한 문제는 모두 직접 구현할 필요 없습니다. 자바 라이브러리에 이미 좋은 메서드들이 구현되어있습니다.
Float.compare(a, b)와 Double.compare(a, b)입니다. 이 두 메서드는 a와 b를 비교해서 a가 더 작으면 -1, 더 크면 1, 같으면 0을 출력합니다. 대소 비교하는 과정에서 위에서 언급한 NaN과 -0.0f를 모두 고려합니다. 대소 비교가 필요하다면 이 메서드를 사용하면 좋습니다.
그런데 이 방법은 동등성 비교시에는 적합하지 않을 수도 있습니다. 위에서 언급한 부동 소수점 문제가 있습니다. 상황에 따라서는 epsilon을 도입하는 방법을 사용해야합니다.
참고
1. 조슈아 블로크, 『Effective Java 3/E』, 프로그래밍 인사이트(2018), p63
2. http://www.tcpschool.com/cpp/cpp_datatype_floatingPointNumber
3. https://stackoverflow.com/questions/1088216/whats-wrong-with-using-to-compare-floats-in-java