1.1+0.1과 1.1+0.2의 차이
사진은 파이썬 콘솔에서 실행한 모습이지만, 파이썬이 아닌 대부분의 프로그래밍 언어에서 비슷한 결과를 보인다.
이 문제의 원인은 컴퓨터 프로그래밍을 맨 처음 배울 때 알게 될 정도로 많은 사람들이 알고 있을 것이다. 원인은 컴퓨터가 소수를 저장하는 방식에 있다. 컴퓨터는 이진수의 부동소수점 방식으로 소수를 저장하기 때문에 우리가 쓰는 십진수 소수를 모두 정확히 표시할 수 없고, 어떤 값들은 근삿값으로 저장할 수밖에 없다.
하지만 사진처럼 어떤 연산은 정확히 나오고, 어떤 연산은 기대하지 않은 결과를 출력하는 모습을 보인다.
그래서 1.1+0.1==1.2은 False이고, 1.1+0.2==1.3은 True가 나오는 과정을 컴퓨터처럼 비트 단위로 계산해보면서 원인을 알아보자.
부동소수점 계산
파이썬의 float 자료형은 C언어의 double처럼, 소수 하나를 표현하기 위해 64비트를 쓴다. 이때
1 bit = 부호 비트(0이면 양수, 1이면 음수)
11 bit = 지수 비트
52 bit = 가수 비트
로 사용한다. 자세한 부동소수점 표현 방식과 연산 방법은 다른 글에서도 많이 나와 있으므로 여기서는 줄인다.
다음 계산을 편리하게 도와주는 사이트 : https://t.hi098123.com/IEEE-754
1.1 + 0.1 계산
먼저 \( 1.1 + 0.1 \) 를 계산하기 위해 1.1과 0.1을 각각 부동소수점 비트로 나타내보자.
등식의 좌변은 십진법, 우변은 이진법으로 나타낸 소수이다.
$$ 1.1 = 1.0\ 0011\ 0011 ... 0011\ 010 \times 2^0 $$
$$ 0.1 = 1.1\ 0011\ 0011 ... 0011\ 010 \times 2^{-4} $$
그리고 부동소수점 덧셈을 위해 0.1의 형태를 다음과 같이 바꾼다.
$$ 0.1 = 0.0\ 0011\ 0011 ... 0011\ 010 \times 2^0 $$
그리고 이진수끼리 1.1+0.1의 덧셈 연산을 진행한 결과는 다음과 같다.
$$ 1.0\ 0110\ 0110 ... 0110\ 100 \times 2^0 $$
이는 \( 1.2 \) 가 아닌 \( 1.2000000000000002 \)이다. 연산 과정에서 알 수 있듯이 \( 1.1 \)과 \( 0.1 \)을 각각 부동소수점으로 나타낼 때 반올림을 하면서 값에 오차가 발생하였다.
\(1.2\)를 이진수로 변환한 참값은
$$ 1.0\ 0110\ 0110 ... \ 0110\ 010 $$
이다. 맨 끝의 한 비트만큼, 즉 \(2^{-52}\)만큼 커진 것이다. \(2^{-52}\approx 0.0000000000000002\)이므로, 파이썬 콘솔의 결과와 일치함을 확인할 수 있다.
1.1 + 0.2 계산
$$ 1.1 = 1.0\ 0011\ 0011 ... 0011\ 010 \times 2^0 $$
$$ 0.2 = 0.0\ 0110\ 0110 ... 0110\ 011 \times 2^0 $$
더하면,
$$ 1.3 = 1.0\ 1001\ 1001 ... 1001\ 101 \times 2^0 $$
1.3에 해당하는 이진수(52번째 값 이후 순환소수가 절삭되었기 때문에 정확히 1.3은 아니다!)가 나오는 것을 알 수 있다. 이 때문에 \( 1.1+0.2 \)는 부동소수점 연산으로 인한 에러가 나지 않는다.
결론
부동소수점 연산을 직접 해 보면서 왜 어떨 때는 틀린 답이, 어떨 때는 맞는 답이 나오는지 확인했다. 에러가 나는 규칙을 찾을 수 있을지 생각해보았지만 반올림이 되는 경우의 수가 너무 많아서 유효한 규칙을 찾을 수 없을 것으로 예상된다.