Home IEEE-754 (부동소수점 표준)
Post
Cancel

IEEE-754 (부동소수점 표준)

이 시리즈는 Hotspot JVM - JSR-392(Java SE 17)를 기준으로 작성되었습니다.

Java에서 float이나 double 타입은 정확한 계산에 사용하면 안 된다고 한다.
왜냐하면 이들 타입은 IEEE-754로 실수를 나타내기 때문이다. 그럼 도대체 IEEE-754로 계산하는 것이 왜 위험한지, 그럼 정확한 계산을 하려면 어떻게 해야 하는지 알아보자.

IEEE-754

Java에는 실수를 표현하는 Primitive Type이 2가지 있는데 32bit로 표현하는 float, 64비트로 표현하는 double이 그것이다. 이 타입들은 IEEE-754방식으로 실수를 표현한다.

IEEE-754는 부동소수점을 표현하는 가장 널리 쓰이는 표준이다. 여기서 부동이란 不動이 아닌 浮動이며 떠다니며 움직인다는 뜻이다. 즉, 고정 소수점과 다르게 소수점의 위치를 유동적으로 표현하는 방식이라고 이해하면 된다.

이 표준은 값을 단 정밀도, 배 정밀도로 구분하는데, 각각 32bit, 64bit로 표현되는 값이다.

구분
단 정밀도최상위 부호 비트, 8비트 지수부, 23비트 가수부
배 정밀도최상위 부호 비트, 11비트 지수부, 52비트 가수부

IEEE-754가 적용된 타입은 정확한 계산에 사용하면 안 된다라는 말을 이해하기 위해서 우선 이 표준이 실수를 어떻게 표현하는지를 알아야 한다.

지금부터 3.625라는 숫자가 단 정밀도로 표현되는 과정을 살펴보자.
변환 과정은 이진수 변환 -> 정규화 -> 정밀도 변환 순으로 진행된다.

이진 기수법

컴퓨터는 2진법을 사용하므로 우리가 사용하는 10진법의 실수를 2진수로 변환해야한다.
정수 부분은 2로 나누면서 구하면 되고, 소수점 이하 부분은 다음 규칙에 따라 구하면 된다.

  • 소수 부분에 2를 곱한다.
  • 곱한 뒤의 정수 부분이 이진수의 값이 된다.
  • 계산은 소수점 이하로만 진행하며 곱한 값이 1.0이 되면 종료한다.

3.625를 아래 규칙에 맞게 계산해보자.

1
2
3
4
5
6
7
8
// 정수 변환
3 % 2 = 1 -> 1
1 % 2 = 1 -> 1

// 소수 변환
0.625 * 2 = 1.25 -> 1
0.25  * 2 = 0.5  -> 0
0.5   * 2 = 1.0  -> 1

규칙대로 계산해보면 3.625의 2진수 표현은 11.101이 된다.

정규화

정규화란 소수점 앞에 1이 하나만 있는 형태로 소수점을 이동시키는 것이다.
이 과정에서 소수점이 이동될 때마다 2의 지수(2진법이기 때문에)를 더하거나 빼줘야 한다.
다음 예를 보면 쉽게 이해될 것이다.

이진수 예시소수점 이동소수점이 이동된 이진수정규화된 값
1101.11왼쪽으로 3칸 이동1.101111.10111 * 23
11.0001왼쪽으로 1칸 이동1.100011.10001 * 21
0.00111오른쪽으로 3칸 이동1.111.11 * 2-3

이제 이진수 11.101을 정규화하면 1.1101 * 21이 된다는 것을 알 수 있다.

정밀도 표현

마지막으로 정규화된 1.1101 * 21 을 정밀도로 표현해보자.
우리는 단 정밀도로 표현하는 것이 목적이므로 다음 규칙대로 32bit로 변환한다.

  • 최상위 부호 비트
    최상위 부호 비트는 양수면 0, 음수면 1.
  • 지수부
    지수에 바이어스값(2n-1 - 1)을 더하고 이진수로 표현한다.
  • 가수부
    소수점 이하 자리수를 그대로 적고 남은 비트 자리들은 0으로 채운다.
지수에 바이어스값을 왜 더할까?
  위 예시 테이블의 0.00111값을 보자. 2-3을 식으로 표현하면 1/23이다. 
  즉, bit로 다시 소수를 표현해야 하는 상황이 발생하는 것이다. 
  따라서 음의 지수가 나오더라도 정수처럼 계산할 수 있도록 바이어스 값을
  더해 0을 음수의 최소값으로 사용하도록 하는 것이다.  

  참고로 32bit의 바이어스 값은 28-1 - 1 = 127, 64bit의 값은 211-1 - 1 = 1023 이다.

1.1101 * 21을 위 규칙대로 변환하면 아래와 같이 변환된다.

최상위 부호 비트지수부 8비트가수부 23비트
01000000011010000000000000000000

그래서 왜 정밀 계산에 쓰지 말라는 걸까?

이제 우리는 실수를 IEEE-754방식으로 변환할 줄 알게 되었다.
그럼 0.1을 단 정밀도로 변환해보도록 하고 우선 이진수로 변환을 해보자.

1
2
3
4
5
6
7
8
9
0.1 * 2 = 0.2 -> 0
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
...

그렇다. 무한 소수가 된다.
이처럼 실수를 이진수로 표현해보면 무한 소수인 경우가 발생한다.
하지만 표현할 수 있는 bit의 개수는 정해져 있기 때문에 무한 소수를 IEEE-754로 변환하는 과정에서 소실되는 부분이 생기고 오차가 발생하게 되는 것이다. 실제로 float이나 double형으로 0.3 + 0.6를 실행해보면 틀린 결과가 나오는 것을 확인할 수 있다.

BigDecimal

정확한 계산이 필요할 때는 BigDecimal타입을 사용하면 된다.
그런데 BigDecimal타입은 어떻게 실수 계산을 정확하게 하는 걸까?

다음과 같이 BigDecimal 객체를 생성해보자.

1
2
BigDecimal a = new BigDecimal("3.625");
BigDecimal b = new BigDecimal("1.300005");

변수 a의 생성자에서는 3.625에서 sacale값 3, precision값 4를 구하고, 정수로 변환해 내부 값을 3625로 저장한다.

➤ scale
    소수점의 전체 자리수. 소수점부터 0이 아닌 가장 오른쪽 숫자까지의 길이.
➤ precision
    숫자의 전체 길이. 0이 아닌 가장 왼쪽의 수부터 0이 아닌 가장 오른쪽 숫자까지의 길이.

즉, 위 예제의 변수 a, b의 내부 상태는 다음과 같다.

변수명내부 값scaleprecision
a362534
b130000567

이제 변수 a에서 변수 b를 더하는 과정을 통해 왜 오차가 나지 않는지 살펴보자.
BigDecimal이 값을 더하는 과정은 다음과 같다.

  1. 피연산자들의 scale값이 다르다면, 10에 scale의 차이만큼 제곱한 후 scale이 작은 쪽의 값에 곱한다.
  2. 값을 더한다.
  3. 더한 값으로 새로운 BigDecimal 객체를 생성한다.

위 과정대로 연산을 진행해보면

  1. a의 scale값 3이 b의 scale값인 6보다 짧으므로, a의 내부 값 3625에 10(6 -3)을 곱해 소수점 이하의 자리수를 맞춘다.
  2. a의 내부 값 3625000에서 b의 내부 값 1300005를 더한다. (3625000 + 1300005 = 4925005)
  3. BigDecimal의 생성자를 통해 내부 값 4925005, scale값 6, precision값 7을 가진 객체를 생성하고 반환한다.

즉, BigDeciaml의 내부적으로 연산을 정수 형태로 하기 떄문에 오차가 나지 않는 것이다.

This post is licensed under CC BY 4.0 by the author.

자바 가상 머신의 원리와 이해 (1) - 데이터 유형 (Java Virtual Machine - Data Types)

자바 가상 머신의 원리와 이해 (2) - 런타임 데이터 영역 (Java Virtual Machine - Runtime Data Areas)