2006년 7월 20일 목요일

C#과 떠다니는 소수점 이야기[3]

오래 기다리셨습니다. 아니, 아무도 안 기다리셨으려나? 뭐, 상관 없습니다. 저는 꿋꿋이 글을 써 나가겠습니다.


이제는 우리가 C# 코드에서 부동 소수점 자료형을 사용할 때 생각해야 하는 부분들에 이야기하려 합니다. 이것들은 C# 뿐 아니라 IEEE 754 규격을 따르는 모든 시스템에 해당 되는 내용입니다.(대체 어떤 이야기길래?)

float a = 0.1f 0.2f;


위 코드에서 a는 얼마가 될까요? 당연히 -0.1이겠지요? 그럼 다음 코드를 봅시다.

float b = 43.1f - 43.2f;


위 코드에서 b는 얼마가 될까요? 하핫, 여러분을 바보로 아냐고요? 아 죄송합니다. 제가 바보 같은 질문을 했습니다. 그럼 다음 코드를 작성해서 한번 실행해 보시기 바랍니다.

using System;

class FloatEx01

{

  static void Main(string[] args)

  {

       float a = 0.1f - 0.2f;

       Console.WriteLine(a);

       PrintOutInHex(a);

       float b = 43.1f - 43.2f;

       Console.WriteLine(b);

       PrintOutInHex(b);

  }

  private static void PrintOutInHex(float value)

  {

       byte[] bytes = BitConverter.GetBytes(value);

       foreach (byte b in bytes)

       {

           Console.Write("{0:X2} ", b);

       }

       Console.WriteLine();

  }

}

결과 :

-0.1

CD CC CC BD

-0.1000023

00 CE CC BD


, 이런. 이게 무슨 일이지! 내가 프로그래밍 언어를 잘못 선택한 게 아닌가?!. 진정하세요. 이것은 IEEE 754를 따르는 모든 시스템에서 나오는 문제입니다.


앞에서 설명한 것처럼 2진수로는 소수를 모두 표현할 수가 없습니다. 예를 들어 0.1을 이진수로 바꾸면 0.00011001100 의 순환 소수가 되지요. 게다가 우리는 한정된 비트 안에 이 수를 표현해야 하므로 비트 크기를 벗어나는 나머지 수들은 저장되지 못하게 됩니다. 그렇다고 나머지 수들을 버릴 수도 없기에 이 값들을 반올림해서 저장합니다. 따라서 우리가 얻을 수 있는 것은 정확한이 아닌 가까운 값 뿐입니다. 이로 인해 위와 같은 현상이 나타나는 것이죠. 이것을 반올림 오류(round off error)라고 합니다.


이를 어찌해야 할까요? 위 예제 코드에서 float으로 사용된 변수를 double로 바꿔 보면 오차가 훨씬 줄어드는 것을 확인할 수 있습니다.

0.1

9A 99 99 99 99 99 B9 3F

0.100000000000001

00 9A 99 99 99 99 B9 3F


그래도 여전히 오차가 존재합니다만, 이 정도면 그래도 꽤 쓸만해 졌습니다. double 형은 64비트(8바이트)의 자료형으로써 11비트의 지수부와 52비트의 가수부로 이루어져 있기 때문에 float 보다 정확한 값을 나타낼 수 있습니다.


double은 8바이트 자료형이니 float 보다 느려지지 않겠냐구요? 두 자료형의 처리 속도는 거의 차이가 없습니다. 메모리를 두 배나 사용하긴 하지만 여러분의 프로그램이 동작해야 하는 PC가 충분한 메모리를 갖고 있다면 double을 사용하는 것이 정신 건강에 이로울 것입니다.

다만, 여전히 아래의 코드는 여전히 실망스러운 결과를 낳죠. not equal을 출력합니다.

double a = 0.2 - 0.1;

double b = 43.2 - 43.1;

if (a == b)

  Console.WriteLine("equal");

else

  Console.WriteLine("not equal");


부동 소수점 자료형을 직접 비교한다는 것은 위험한 일입니다. 정 비교가 필요하다면 오차 허용치를 이용하여 근사값끼리 비교하는 방법이 있습니다. 아래와 같이 말이죠.

const double EPSILON = 0.0001; // 허용오차

private bool isEqual(double x, double y) // 비교 함수.

{

  return (((x - EPSILON) < y) && (y < (x + EPSILON)));

}

//

if (isEqual(a,b))

Console.WriteLine("equal");

else

  Console.WriteLine("not equal");


컴퓨터가 소수 하나도 정확하게 처리를 못한다는 것은 정말 슬픈 일입니다. 그러나 이것은 현재 CPU의 구조상 어쩔 수 없는 일입니다. 우리가 할 수 있는 것은 최대한 정확하게 데이터를 처리하기 위해 float 대신 double을 사용하는 것과, 이 오류와 관련해서 생길 수 있는 프로그램의 영향을 줄이는 것뿐입니다.

하지만 C#은 또 하나의 해결책을 제시합니다. 바로 decimal형입니다. 이 자료형은 부동 소수형이 아니며, 29 자리수의 수를 지원합니다. 소수점은 29자리 안에서 이동합니다. 반올림 오류가 일어나서는 안 되는 재부/금융/회계 분야에 적합한 자료형입니다. 다만 그 크기가 16바이트나 되고 부동 소수점 자료형보다 속도가 훨씬 느리다는 단점이 있습니다. 이러한 Trade-Off는 어쩔 수 없습니다. 정확한 계산을 위해서라면 속도와 메모리는 조금 희생해야죠. ^^;

, 이렇게 해서 부동 소수점에 대해 이야기를 나눠봤습니다. 역시나 재미가 없죠? 다음에는 비교에 대해 이야기를 해볼까 합니다. 그럼 즐프하세요~


(이 포스트는 추후 수정될 수도 있습니다.)

댓글 없음:

댓글 쓰기