함정 시리즈

부동 소수점 연산의 함정

노새두마리 2023. 11. 19. 00:55
모르면 언젠가 한 번은 당할 수밖에 없는 것

0.1 + 0.2 === 0.3

그동안 컴퓨터로 계산한 0.1 + 0.2의 결과가 0.3이 아니라는 정도로만 알고 있었지 실제로 크게 당해본 적은 없었습니다.

보통의 경우에는 값이 안 맞는다 싶을 때, 적절한 소수점 자리에서 반올림으로 처리하면 올바르게 보정됩니다.


반올림은 괜찮지만...

이 미세한 오차로 인한 진짜 문제는 반올림 연산이 아닌 내림 연산 또는 올림 연산을 사용할 때 본격적으로 발생합니다.

위의 수를 소수점 아래 셋째 자리에서 반올림 · 올림해야 한다고 생각해 봅시다.

자바스크립트로는 아래와 같이 수행할 수 있습니다.

let number = 1.1 * 1.1;
number;                         // 1.2100000000000002
Math.round(number * 100) / 100; // 1.21 올바르게 보정됨
Math.ceil(number * 100) / 100;  // 1.22 오차가 극대화됨
  • 오차의 영향을 받는 자릿수에서 반올림 연산을 수행하면 실제 결과와 동일해짐
  • 실제 결과보다 미세하게 작은 값에 대하여 오차의 영향을 받는 자릿수에서 내림을 수행하면 실제 값보다 작아짐
  • 반대로 실제 결과보다 미세하게 큰 값에 대하여 오차의 영향을 받는 자릿수에서 올림을 수행하면 실제보다 값이 커짐

오차를 피하는 방법

소수 연산을 정수 연산으로 바꾸어 수행합니다.

  • 전체 식에 10의 거듭제곱을 곱하여 소수점 아래 부분을 정수 부분으로 끌어올립니다.
  • 연산을 수행합니다.
  • 연산의 결과를 처음 곱했던 10의 거듭제곱으로 다시 나눕니다. 

예를 들어, 0.1 + 0.2를 계산하고 싶다면 다음의 과정을 따릅니다.

  • 두 수에 10을 곱합니다.
  • 1 + 2를 계산합니다.
  • 3이라는 결과를 다시 10으로 나눕니다.

0.3을 얻었습니다.


무한소수는요?

1/9 같은 0.1111... 무한소수는 어떻게 하죠? 10의 거듭제곱을 아무리 곱해도 0보다 작은 부분이 남아 있는 걸요.

10의 거듭제곱을 곱하는 것의 핵심은 모든 소수부를 정수로 만들어서 계산하는 것이 아니라 특정 자릿수까지 부동 소수점 오차의 영향을 받지 않도록 하는 것이므로 10의 거듭제곱을 곱한 뒤 소수부가 남아 있는 것은 크게 상관 없습니다.


핵심

식에 10의 n제곱을 곱해서 계산한 뒤 다시 10의 n제곱으로 나눈다면, 적어도 정수로써의 연산이 진행되었던 소수점 아래 n자리까지는 부동 소수점 오차의 영향을 받지 않습니다.

정수로써 계산된 자리는 부동 소수점 오차의 영향을 받지 않는다.

번외: 부동 소수점 오차를 포함한 채로 값 비교하기

두 값의 차이가 프로그래밍 언어가 표현할 수 있는 가장 작은 수 보다 작은지 비교합니다.

// JavaScript의 경우, Number.EPSILON이 가장 작은 수에 해당합니다.

Math.abs(0.3 - (0.1 + 0.2)) < Number.EPSILON; // true

입실론이 양수라서 절댓값으로 변환하지 않아도 참이 나오는 것 같네요. (신기)

절댓값을 사용하여 비교하도록 합시다. 음수인 경우에 무조건 참이 되니까요.


다 같이 함정에 빠진 모습

 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON