멈추지 않고 끈질기게

[컴퓨터 공학] 0.11f * 3 == 0.33f ? 본문

컴퓨터공학

[컴퓨터 공학] 0.11f * 3 == 0.33f ?

sam0308 2023. 2. 3. 11:43

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

 

이번 포스팅에서는 컴퓨터에서의 소수 표현에 대해서 간단하게 알아보겠습니다.

 

1. 서문

bool result = (0.11f * 3 == 0.33f);
Console.WriteLine(result);

상기 코드는 C#에서 0.11f * 3 == 0.33f 의 결과를 출력하는 코드입니다. 상식적으로 0.11 x 3이면 0.33이니까 당연히 true일 것 같지만, 막상 실행해보면 좀 다릅니다.

 

그림 1. (0.11f * 3 == 0.33f)의 결과

보다시피 False를 출력하고 있습니다. 이런 현상이 발생하는 이유를 이해하려면 2진법과 소수점 자료형의 특징을 이해해야 합니다.

 

 

2. 컴퓨터의 소수 저장 방식

컴퓨터에서 소수를 저장할 때는 IEEE에서 제안한 부동소수점 방식을 사용합니다. 저장하려는 숫자를 부호, 지수부, 가수부로 나누어 저장하는 방식입니다. -125.125(-1111101.001)를 32비트에 저장하는 경우를 예로 들어보겠습니다.

 

1) 저장하려는 수의 부호를 저장합니다(양수는 0, 음수는 1).

 

2) 저장하려는 수의 절대값에서 소수점을 옮겨 소수점 왼쪽에 1만 남는 형태로 표현합니다(1.111101001 * 2⁶). 

 

3) 곱하는 지수 값을 지수부에 저장합니다. IEEE 표준에서는 지수에 127을 더한 값(6 + 127)을 저장합니다.

 

4) 소수점 오른쪽 부분을 가수부에 저장합니다. 이 때 남는 비트는 0으로 채워넣습니다.

 

문제는 무한소수가 발생하는 경우입니다. 프로그래밍 언어에서 소수를 저장할 수 있는 자료형은 float, double 등이 있으며, 이러한 자료형들은 float는 32비트, double은 64비트 등 그 사이즈에 한계가 있습니다. 위와 같은 32비트 부동소수점 표현 방식에서는 가수부에 최대 23비트까지 저장할 수 있고, 그 밑으로는 버려지게 됩니다. 따라서 무한 소수를 저장할 경우 우리가 생각하는 값과 실제 저장된 값 사이에 근소한 차이가 발생하게 됩니다.

 

10진법으로 먼저 예를 들어보겠습니다. 1/3은 소수로 표현하면 0.3333..... 으로 표현되는 무한 소수입니다. 이를 저장할 때 소수점 8자리까지만 기록할 수 있다면, 실제 저장되는 값은 0.33333333 이 됩니다. 이 값에 3을 곱하면 우리는 1/3 * 3 = 1을 예상하겠지만, 실제로는 0.99999999 가 되므로 0.00000001 의 차이지만 정확히 1은 아닙니다.

 

우리에게 익숙한 10진법의 경우에는 무한 소수가 발생하는 경우를 비교적 예측하기 쉽지만, 문제는 컴퓨터는 2진법을 사용한다는 점입니다. 0.11과 같은 딱 떨어지는 소수를 가지고 계산한다고 생각하지만 실제로 컴퓨터는 이를 2진수로 저장하기 때문에 무한 소수가 되고, 무한 소수를 유한한 비트 수 내에 저장하기 때문에 버려지는 부분이 발생하여 10진법으로 생각한 결과와 다른 결과가 나오는 것입니다.

 

3. 해결 방법

단순한 해결 방법으로는 사이즈가 큰 자료형을 사용하는 방법이 있습니다. 제목의 (0.11f * 3 == 0.33f)에서 f를 지워 float가 아닌 double 타입으로 연산했을 때 결과를 출력해보면 true가 나오는 것을 알 수 있습니다. 사이즈가 큰 자료형일수록 가수부의 크기도 크고, 따라서 버려지는 소수점의 크기가 훨씬 작아지기 때문에 오차가 줄어드는 것입니다. 다만 모든 소수를 double 타입 이상으로 저장한다면 메모리를 불필요하게 많이 차지하게 됩니다. 또한 오차가 줄어드는 것이지 완전히 없어지는 것도 아니므로, 근본적인 해결책은 되지 못합니다.

 

가장 확실한 방법은 소수 연산을 지양하는 것입니다. 게임이라면 가급적 내부적인 데미지 계산 등은 정수로 연산하도록 하고, UI에서만 소수로 표현하는 것입니다. 예를 들어 데미지를 20% 증가시킬 경우 계수로 1.2를 곱해주면 간단하지만, 대신 120을 곱하고 실제 체력 감소 및 데미지 표기 노출은 결과값을 100으로 나눈 몫을 사용하는 것입니다. 또한 혹여 소수를 사용하더라도 10진법으로 생각한 결과와 다소 오차가 있다는 점을 인지하고, 특히 소수끼리 등호로 비교하는 상황은 피해야 합니다.