Java - Error in Floating Point

  • 0.1 + 0.2 == 0.3 이 아니다??
  • 우리가 프로그래밍을 할 때 소수점을 표현하는 방식은 크게 두 가지가 있다.
  • 하나는 고정 소수점 방식이고, 하나는 부동 소수점 방식이다.

1. 고정 소수점 방식

  • 프로그램은 1과 0으로 모든 것을 표현한다. 정수에서는 byte 라는 8비트짜리 자료형에서 3을 표현할 때 0 0 0 0 0 0 1 1 이렇게 표현하듯 소수점도 마찬가지이다. 고정 소수점 방식은 32비트에서 미리 비트수를 정하고, 그 비트만을 활용하여 실수를 표현하는 방식이다.
    • 처음 1비트는 양수, 음수를 구별하며 ( 0일 때 양수, 1일 때 음수 )
    • 다음 15비트는 정수부
    • 다음 16비트는 소수부이다.
  • 2진수로 변환된 수를 그대로 넣으면 되고, 남은 수는 0으로 채운다.
    • 예를들어 7.625를 32비트 고정소수점 방식으로 표현하면, 2^2 + 2^1 + 2^0 + 1/2^1 + 1/2^3 이기 때문에 111.101이 되고,
    • 0 / 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 / 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 이렇게 자리를 차지한다.
  • 고정 소수점 방식은 Ada 와 같은 언어에서 사용되며, 소수에 사용할 비트가 16비트로 고정되어있기 때문에 큰 실수를 표현하기 어렵다.



2. 부동 소수점 방식

  • 고정 소수점 방식의 단점을 보완하기 위해 등장한게, 부동 소수점 방식이다.
  • 부동 소수점은 2진수를 정규화 하여 사용한다
  • float (32비트) 기준
    • 처음 1비트는 양수, 음수를 구별하고
    • 다음 8비트는 지수부
    • 다음 23비트는 가수부로 활용한다.
  • double (64비트) 기준으로
    • 처음 1비트는 양수, 음수를 구별하고
    • 다음 11비트는 지수부
    • 나머지 52비트는 가수부로 활용한다.


부동 소수점 추가 설명

  • float를 기준으로 보면, 여기서 정규화라는 것은 2진수를 1.xxx … * 2^n 형태로 나타내는 것을 말한다.
  • 예를 들어 7.625를 부동 소수점 방식으로 나타내보자.
    • 정규화 이전에는 111.101이 나온다.
    • 정규화를 하면, 1.11101 * 2^2 가 된다.
      • 111.101이 1.11101이 되기 위해 소수점을 2번 앞으로 당겼기 때문에, 그만큼의 지수가 생긴것이다.
    • 해당 지수에서 IEEE 754 표준에 의거하여 127이라는 bias를 더한 값으로 2진수를 채워 지수부인 8비트를 완성해야 한다.
    • 따라서 2 + 127은 129가 되므로, 1 0 0 0 0 0 0 1 이 지수부 8비트에 들어가는 것이다.
      • 참고로 11비트를 지수부로 가진 double에서는 1023을 더한다.
    • 이렇게 bias를 더하는 이유는 지수가 음수일 경우를 대비하기 위해서이다.
      • 0.000101 이라는 수를 정규화 하면 1.01 * 2^-4 가 된다. 그러면 -4를 지수부에 표현할 방법이 없으므로, +127을 통해 123을 만들어서 0 1 1 1 1 0 1 1 로 만든다. 그래서 지수 + bias를 더했을 때 0 ~ 127이 나오면 음수, 128 ~ 255가 나오면 양수라고 보면 된다. ( 실제 값은 -127 ~ 0 / 1 ~ 128 )
    • 그리고 뒤에 나머지 가수부 비트에 소수를 이진수 화 한 값을 넣어주면 된다.
    • 7.625는 결국 0 / 1 0 0 0 0 0 0 1 / 1 1 1 1 0 1 0 0 0 0 ……… (32비트까지) 이렇게 표현할 수 있는 것이다.



3. 부동 소수점의 오류

  • 왜 0.1 + 0.2 는 0.3이 아닐까?
  • float 기준으로 23비트까지 가수부로 활용할 수 있다. 우리가 0.5를 이진수로 바꾸면, 0.1 이고, 0.25를 이진수로 바꾸면 0.01이 되며, 0.125는 0.001이 된다. 이처럼 딱 떨어지는 수라면 부동소수점에서 오류가 발생하지 않는다.
  • 그러나 0.3은 어떨까?
  • 0.3 을 2진수로 변환하면, 0.0100110011 . . . 이렇게 특정 수가 무한하게 반복된다. 그러다가 비트가 다 채워지면 거기서 강제로 끊어지게 되므로, 그 뒤에 값만큼의 오차가 발행하게 된다. 그래서 근사치로 표현되는 것이다.
  • float에 0.3이 담겼다고 해서, 실제로 0.3의 값이 담긴게 아니라, 0.3과 근접한 값이 담긴 것 뿐이다.
    • 그래서 프로그래밍에선 0.1 + 0.2는 0.3이 아니라고 하는 것이다.
  • 즉, 우리는 실수에 대한 어떤 연산등을 할 때 항상 직관과 다른 결과가 발생할 수 있다는 것을 염두해 두어야 한다.



4. 그럼 소수점 계산은 어떻게 할까?

  • Java에서는 소수점 계산을 위해 BigDecimal 이라는 클래스를 지원해준다.
  • 이는 금융권처럼 소수 계산을 많이하는 곳에서는 필수적으로 알아야하는데, 여기서 사용되는 메서드나 사용 방법은 다른 포스팅에서 하고, 오늘은 어떻게 BigDecimal이 부동소수점의 오류를 보완하는 지를 알아보고자 한다.
  • 예를 들어 100.234 라는 값을 BigDecimal에 담는다.
    • intVal : 정수부 값, = null
    • scale : 소수부의 길이 = 3
    • percision : 정수부 소수부의 길이(length)의 합 = 6
    • stringCache : 숫자를 String으로 변환한 값 = “100.234”
    • intCompace : 소수점을 제외한 전체 수 = 100234
  • 즉, String과 어디에 소수점이 찍혔는지 등을 저장하고, 마치 정수형처럼 계산할 수 있게 해주는 것이다.

예시

  • 100.235 + 100.235를 한다고 치자
  • 그러면 정수부분인 100 + 100을 따로 해주고, 소수부인 0.235 와 0.235를 정수처럼 235 235로 만들어 + 로 계산을 해준다.
  • 그러면 200과 470이 나오고, 이걸 소수점을 찍어 합쳐주면 200.470 이 나오게 된다.
  • 이러면 부동소수점의 오류 없이 계산을 할 수 있다.






results matching ""

    No results matching ""