카테고리 없음

고정 소수점과 부동 소수점의 오차 알아보기

monkeyDugi 2022. 12. 21. 22:05
반응형

float, double, BigDecimal

정수 값의 크기와 범위


1비트당 정수는 0과 1로 2개를 표현할 수 있고, 4개이면 2^4로 16개이고,

8비트이면 1바이트로 2^8으로 256개이다. 그래서 실제로 8비트는 0 ~ 255의 숫자를 표현할 수 있다.

그렇다면, 음수를 표현하기 위해서는 아래 그림과 같이 가장 앞의 1비트는 부호 비트로 사용하고 나머지 7개 비트만 사용하게 된다.

결국 -2^7 ~ 2^7 - 1의 정수까지 표현이 가능하게 된다.

2^7은 128이므로 음수 128개 양수 128개를 표현할 수 있는 것이다.

즉 -128 ~ 127까지 표현이 가능하게 되는 것이고, 음수가 128이고 양수가 127인 이유는

간단하게 보면, 음수까지 -2^7 + 1로 시작하여 -127 ~ 0까지하면 양수 포함하는 0 ~ 127의 숫자 중

0이 겹치기 때문이다. 그래서 음수에 혜택?을 준것이다.

부동 소수점(Float Point)


고정 소수점(Fixed Point)이란?

고정 소수점의 단점은 정수부가 -2^15 ~ 2^15 - 1(32,768)로 너무 적다는 것이다.

하지만 소수부는 너무 많이 남아있게 되므로 효율적이지 못하다.

그래서 부동 소수점이 등장하게 되었다.

부동 소수점(Float Point) 이란?

정수부의 범위가 너무 적어서 표현할 수 있는 범위가 적기 때문에 소수점을 이동하여

범위를 넓히기 위한 개념이다. 즉, 가수부에서 정수와 소수를 모두 표현하기 위한 것으로

컴퓨터는 이 방법을 자리가 많아지면 채택하고 있다.

부동 소수점 구조 알아보기

아래의 표기법을 IEEE 754 stadard라고 한다.

용어 정리

  • S: 부호(Sign bit)
    • 1bit이고, 0이면 양수, 1이면 음수
  • E: 지수(Exponent)
    • 소수점이 몇자리 이동한지를 표현하는 정수로써
      범위는 -127 ~ 128(float), -1023 ~ 1024(double)
  • M: 가수(Mantissa)
    • 실제 값을 저장하는 부분
    • 10진수로 7자리(float), 15자리(double)의 정밀도로 저장 가능.

예로 아래 네 가지 케이스를 부동 소수점으로 변환할 경우를 알아보자.

편의를 위해 이진수가 아닌 십진수로 표현하겠다.

실제로는 이진수로 저장이 된다.

float과 double의 표현 범위

  • 정밀도: 소수 자리의 정밀도가 아닌 실수 자체의 정확한 표현 자리수를 의미한다.

  • 정밀도 코드로 확인 해보기

      @Test
      void float_정밀도_확인() {
              float f1 = 9.12345678901234567890f;
    
          /**
           * 9.123457 -> 7자리까지 정밀함. 반올림으로 마무리
           * 이건 실제값이 이런건 아니고, print하기 위해 자동으로 반올림이 된 것뿐이다.
           */
          System.out.println("f1 = " + f1);
    
          /**
           * 이게 실제로 저장된 값이다.
           * 9.12345695495605500000 -> 7자리까지만 정확히 일치
           * 정밀도가 7자리이기 때문에 나머지 수는 일치하지 않는 현상이 발생한다.
           * 간혹 8자리까지 일치하는 경우가 있으나 드문 경우라서 7자리라고 생각하면 된다.
           */
          System.out.printf("f1 = %24.20f%n", f1);
      }
    
      @Test
      void double_정밀도_확인() {
          double d = 9.12345678901234567890;
    
          /**
           * 9.123456789012346 -> 15자리까지 정밀함.
           */
          System.out.println("d = " + d);
    
          /**
           * 9.12345678901234600000 -> 15자리까지만 정확히 일치
           */
          System.out.printf("d = %24.20f%n", d);
      }

실수 오차가 발생하는 이유

IEEE 754 standard에 의해 컴퓨터는 실수 번역을 한다.

이건 위에서 설명한 부동 소수점 구조 알아보기에서 설명한 구조이다.

고정 소수점이 아니기 때문에 결국 근사치로 계산을 하게 된다.

컴퓨터는 모든 것을 이진수로 변환하기 때문에 만약 6.5를 변환한다고 이는 딱 맞아 떨어지게 된다.

하지만 6.4같은 경우는 무한 소수가 된다. 그래서 가수부에 모두 담을 수 없기 때문에 짤리는 부분이

생겨서 오차가 발생한다.

예시1

@Test
void ddd() {
    double a = 0.1;
    double sum = 0;
    System.out.println("a = " + a); // 0.1

    for (int i = 0; i < 10; i++) {
        sum += a;
    }

    System.out.printf("%.15f\n", sum); // 1.000000000000000
    System.out.printf("%.16f\n", sum); // 0.9999999999999999

    // cancel 출력
    if (sum == 1.0) {
        System.out.println("ok");
    } else {
        System.out.println("cancel");
    }
}

a라는 변수는 일단 0.1로 출력이 된다. 하지만 실제로 이진수로 변환했을 때 무한 소수가 되어서

실제로 연산 시에는 오차가 발생하게 된다. 15자리까지 sum을 출력했을 때는 1.0000…이 나오지만

double의 정밀도 범위는 벗어나는 16자리까지 출력하게 되면 오차가 발생한 것을 볼 수 있다.

그래서 1.0으로 생각하고 if문을 실행하면, 컴퓨터가 실제로 인지하고 있는 0.9999…로 계산되기 때문에

cancel이 출력되게 된다. 가능하면 실수는 사용하지 말고 써야한다면, float은 7자리, double은 15자리

맞춰서 쓰도록 해야한다. 그렇지 않으면 이후 오차가 있는 부분이 짤리기 때문에 다른 값임에도 같은 값으

처리가 될 수도 있다.

예시2

@Test
void 근사치_확인하기() {
    // given
    float num1 = 0.3f;
    float num2 = 0.4f;

    float sum = num1 + num2; // 0.70000005

    if (sum == 0.7) {
        System.out.println("sum은 0.7이다");
    } else {
        System.out.println("sum은 0.7이 아니다"); // 이게 출력됨.
    }
}

이번에는 IEEE standard 754 Converter를 이용해서 알아보자.

0.3, 0.4, 0.7을 IEEE에 맞춰 변환하면 아래와 같이 나온다. 이는 IEEE standard 754 Converter

사용한 사진이다.

0.7만 확인해보면, 근사치인 0.6999..가 나오는 것을 볼 수 있다.

0.3, 0.4는 0.3…., 0.4…로 변환된다.

이와 같은 이유로 sum은 0.7이 아닌 것이다.

float, double, BigDecimal은 언제 사용해야 할까?


정확한 답이 필요한 계산에는 float, double을 피해라.

  • 근사치를 계산하기 때문에 정확한 계산이 되지 않는다.
  • double: 보통 더 큰 수를 표현하기 보다는 더 정밀한 정밀도를 필요로 할 때 사용한다.
  • float: double이 필요없을 경우 보통 사용한다.

BigDecimal

  • 소수점 추적은 시스템에 맡기고, 코딩 시 불편함이나 성능 저하를 신경쓰지 않을 경우
  • 반올림을 완벽히 제어하는 8가지 기능을 제공.
  • 법으로 정해진 반올림을 수행해야 하는 비지니스 계산에서 아주 편리한하다.
  • 십진수 18자리 초과할 수 있음

int, long

  • 성능이 중요하고 소수점을 직접 추적할 수 있고, 숫자가 너무 크지 않을 경우
  • int: 십진수 9자리 이하
  • long: 십진수 18자리 이하

오차 해결하기 위한 방법


BigDecimal을 활용할 수 있다.

@Test
void BigDecimal로_오차_해결() {
    // given
    BigDecimal num1 = BigDecimal.valueOf(0.3);
    BigDecimal num2 = BigDecimal.valueOf(0.4);

    BigDecimal sum = num1.add(num2);
    System.out.println("sum = " + sum);

    if (sum.equals(BigDecimal.valueOf(0.7))) {
        System.out.println("sum은 0.7이다"); // 출력
    } else {
        System.out.println("sum은 0.7이 아니다");
    }
}

정수로 변환하기

@Test
    void 부동_소수점으로로_오차_해결하기() {
        // given
        float num1 = 0.3e3f; // 300.0
        float num2 = 0.4e3f; // 400.0

        float numE6 = 0.7f * 1e3f;
        System.out.printf("%.15f\n", numE6); // 700.0000...
        System.out.printf("%.15f\n", numE6 / 1e3); // 0.7000...

        float num = 0.7f;
        System.out.printf("%.15f\n", num); // 0.699999988079071

        float sum = num1 + num2;
        System.out.println("sum = " + sum);

        if (sum == 0.7e3) {
            System.out.println("sum은 0.7이다"); // 출력
        } else {
            System.out.println("sum은 0.7이 아니다");
        }
    }

참고 자료


https://www.youtube.com/watch?v=ly-J4FsOJJE

https://www.youtube.com/watch?v=XlzKvh6Plms

https://medium.com/@pranne1224/프로그래밍에서-정확한-소수점-계산은-어떻게-할까-2a61fa60e002

반응형