실수의 표현방식 - 부동소수점(IEEE 754)
부동소수점
정의
- 부동(떠다니는) 소수점이라는 의미입니다.
- Java에서는 IEEE 754 표준을 따라 실수를 표현합니다.
- float은 32비트, double은 64비트로 구성됩니다.
구성
부호비트
- 1비트의 크기를 가집니다.
- 실수의 부호를 결정합니다.
- 0을 양수비트, 1을 음수비트로 합니다.
지수부
- float에서는 8비트, double에서는 11비트를 가집니다.
가수부
- float에서는 23비트, double에서는 52비트를 가집니다.
부동소수점 방식으로 실수 표현하기
- 실수 13.8을 부동소수점으로 표현합니다.
정수부
- 십진수 : 13
- 이진수 : 00001101
소수부
-
십진수 : 0.8
-
이진수 : 110011001100....(1100 무한 반복)
2로 곱해서 발생한 carry를 모웁니다.
2로 곱해서 소수점 부분이 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
0.6 2 = 1.2 ... 1
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
.
.
.
(무한 순환소수, 순환마디 1100)
무한 소수, 혹은 엄청 긴 소수점을 가지는 실수는 오차를 낼 수 밖에 없습니다.
아래 부동소수점으로 표현하는 방식을 보면 알 수 있습니다.
-
이진수 : 1101.1100 1100 1100 1100 1100 1100 1100....(13.8)
-
소수점 이동(부동) : 1.1011 1001 1001 1001 1001 1001 1001 ... * 2^3
(소수점을 제일 큰 자리수인 1의 뒤까지 이동합니다.)
-
지수부 정규화 :
- 소수점의 이동 칸 수 + bios => 지수부
- 3(0000 0011) + 127(0111 1111) => 130(1000 0010)
-
가수부
- 1011 1001 1001 1001 1001 1001 1001 ......
- 소수점 아래로부터 오른쪽으로 23bit만 사용합니다.
- 1011 1001 1001 1001 1001 100
-
float형에 할당
부호비트 지수부(8bit) 가수부(23bit) 0 1000 0010 1011 1001 1001 1001 1001 100
-
위의 정규화 과정에서 소수점 아래 숫자의 일부가 유실됩니다.
-
예를 들어 어떤 양의 실수를 부동소수점으로 위 과정을 통해 표현하면 소수점을 왼쪽으로 이동시키게 되고, 그로 인해서 원래 정수부에 속하였던 비트들도 가수부로 들어가게 됩니다.
-
그리고 float의 가수부 최대 비트 수(23bit)에 맞춰 들어가면서 나머지 bit는 잘리게 됩니다.
즉, 소수점을 따라서 23자리가 좌측으로 오프셋 되고, 이동 후 남은 자리만큼 0으로 채워지는 것이죠.
-
이진수의 LSD(Least significant digit)은 0이므로 밀린 만큼 0으로 채워지거나 잘립니다.
이때 부동소수점으로 표현되어있는 데이터를 십진수 값으로 되돌리면 위의 과정에서 0으로 채워진 만큼 오차가 발생합니다.
오차가 나면 생기는 일
- 금융, 과학 등 작은 숫자가 중요한 분야에서는 이런 실수의 표현방식으로 나오는 오차가 큰 영향을 끼칩니다.
- 2022년 4월 19일 날짜로 비트코인은 1฿에 50,733,000원 이네요.
가난한 폴리는 코인을 1฿ 단위로 살 수가 없습니다.
(2010년으로 돌아가 과거에 폴리에게 코인을 사라고 말하고 싶네요;)
우선 한화로 50,000원을 매수한다고 가정해보겠습니다.
이 돈으로 매수한 코인은 0.00099168฿ 밖에 안되지만... 저에겐 아주 소중합니다.
@Test
public void bitCoinTest(){
double 오만원으로_산_비트코인_개수 = 0.00099168; // 현 시세
double 열아홉배_수익_가즈아 = 오만원으로_산_비트코인_개수 * 19;
double 비트코인_떡락이_두려워_판매한_개수 = 오만원으로_산_비트코인_개수 * 18;
double 남은_비트코인_개수 = 열아홉배_수익_가즈아 - 비트코인_떡락이_두려워_판매한_개수;
Assertions.assertThat(남은_비트코인_개수).isEqualTo(오만원으로_산_비트코인_개수);
}
/*
org.opentest4j.AssertionFailedError:
expected: 9.9168E-4
but was: 9.916799999999983E-4
Expected :9.9168E-4
Actual :9.916799999999983E-4
*/
-
위 코드를 보면 증가시킨 만큼 뺏기 때문에 원래 값으로 돌아와야하지만, 아주 작은 오차를 발생시킵니다.
0.00099168(9.9168E-4) <- 계산 전
0.0009916799999999983(9.916799999999983E-4) <-계산 후
-
double(가수부 52bit)의 percision는 float(가수부 23bit)보다 정밀하다해도 이렇게 미묘한 값의 오차를 만드는 것은 다름 없습니다.
-
이는 실수의 표현방식에서 오는 한계이기 때문에 32비트던, 4비트던 오차를 만들 수 밖에 없습니다.
숫자에 민감한 분야는 당연히 float은 사용하면 안되겠고, double을 사용하더라도 rounding rule이 필요할 것으로 생각됩니다.
-
다른 방법으로는 BigDecimal Class를 사용하는 것이 있습니다.
BigDecimal
정의
-
위와 같은 정밀도 문제에 대응할 수 있는 java.math 패키지 밑의 클래스 입니다.
-
Java 문서를 보면 다음과 같이 설명되어있습니다.
A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.
-
위의 내용으로 보면 BigDecimal은 2가지 파트로 구성되어 있음을 알 수 있습니다.
1) arbitrary precision integer unscaled value
2) 32-bit integer scale.
-
1)을 해석하자면 "임의의 정밀도를 가지는 정수값"정도가 됩니다.
쉽게 말하면 "임의의 길이를 가지는 정수"라 보면 될 것입니다.
-
2)는 32bit 정수값으로 된 "소수점이 찍히는 위치"정도로 이해하시면 되겠습니다.
-
말로만 표현하자니 햇갈릴 것 같아 수식과 예시를 보이겠습니다.
-
이에 대해서 예로 보자면 다음과 같습니다.
4.904894823467456456345 = 4904894823467456456345 * 10^-21
// 정수 * 10^-왼쪽부터 이동한 소수점의 칸 수
주의
- BigDecimal class의 생성자는 여러개로 overloading되어있습니다.
- 여기서 주의할 점은 double이나 float을 받는 생성자를 사용할 경우 위 계산식에 의해 오차가 발생한 값으로 BigDecimal 객체가 생성됩니다.
- 코드로 보면 아래와 같습니다.
@Test
@DisplayName("두 BigDecimal 객체를 비교합니다.")
public void towBigDecimalEqualsTest(){
BigDecimal bigDecimalMadeOfFloat = new BigDecimal(0.1289371f);
BigDecimal bigDecimalMadeOfString = new BigDecimal("0.1289371");
Assertions.assertThat(bigDecimalMadeOfFloat).isEqualTo(bigDecimalMadeOfString);;
}
/*
org.opentest4j.AssertionFailedError:
expected: 0.1289371
but was: 0.1289370954036712646484375
Expected :0.1289371
Actual :0.1289370954036712646484375
*/
- 같은 0.1289371으로 생성한 것으로 착각할 수 있겠지만, bigDecimalMadeOfFloat의 경우 0.1289371을 32bit 부동소수점으로 변형하면서 근사값으로 표현되고(0.1289370954036712646484375) 이것으로 BigDecimal객체를 생성한 것입니다.
- bigDecimalMadeOfString은 String 내부의 배열을 순회하면서 한 문자씩 가져와 입력한 값(0.1289371)과 같은 BigDecimal객체를 만듭니다.
이에 대한 자세한 동작은 다음에 살펴보도록하겠습니다
결론
- 이번 문서에서는 자바가 소수를 표현하는 방식, 한계와 문제점, 대안에 대해서 살펴보았습니다.
- 컴퓨터의 실수 계산에 대한 이해가 있어야 개발을 함에 있어 어쩌면 일으킬수도 있는 실수를 피할 수 있고, 발생한 버그에 대한 해결책도 합리적으로 찾을 수 있을 것 같습니다.
Author And Source
이 문제에 관하여(실수의 표현방식 - 부동소수점(IEEE 754)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@indongcha/실수의-표현방식-부동소수점IEEE-754
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
십진수 : 0.8
이진수 : 110011001100....(1100 무한 반복)
2로 곱해서 발생한 carry를 모웁니다.
2로 곱해서 소수점 부분이 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
0.6 2 = 1.2 ... 1
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
.
.
.
(무한 순환소수, 순환마디 1100)
무한 소수, 혹은 엄청 긴 소수점을 가지는 실수는 오차를 낼 수 밖에 없습니다.
아래 부동소수점으로 표현하는 방식을 보면 알 수 있습니다.
이진수 : 1101.1100 1100 1100 1100 1100 1100 1100....(13.8)
소수점 이동(부동) : 1.1011 1001 1001 1001 1001 1001 1001 ... * 2^3
(소수점을 제일 큰 자리수인 1의 뒤까지 이동합니다.)
지수부 정규화 :
- 소수점의 이동 칸 수 + bios => 지수부
- 3(0000 0011) + 127(0111 1111) => 130(1000 0010)
가수부
- 1011 1001 1001 1001 1001 1001 1001 ......
- 소수점 아래로부터 오른쪽으로 23bit만 사용합니다.
- 1011 1001 1001 1001 1001 100
float형에 할당
부호비트 | 지수부(8bit) | 가수부(23bit) |
---|---|---|
0 | 1000 0010 | 1011 1001 1001 1001 1001 100 |
위의 정규화 과정에서 소수점 아래 숫자의 일부가 유실됩니다.
예를 들어 어떤 양의 실수를 부동소수점으로 위 과정을 통해 표현하면 소수점을 왼쪽으로 이동시키게 되고, 그로 인해서 원래 정수부에 속하였던 비트들도 가수부로 들어가게 됩니다.
그리고 float의 가수부 최대 비트 수(23bit)에 맞춰 들어가면서 나머지 bit는 잘리게 됩니다.
즉, 소수점을 따라서 23자리가 좌측으로 오프셋 되고, 이동 후 남은 자리만큼 0으로 채워지는 것이죠.
이진수의 LSD(Least significant digit)은 0이므로 밀린 만큼 0으로 채워지거나 잘립니다.
이때 부동소수점으로 표현되어있는 데이터를 십진수 값으로 되돌리면 위의 과정에서 0으로 채워진 만큼 오차가 발생합니다.
가난한 폴리는 코인을 1฿ 단위로 살 수가 없습니다.
(2010년으로 돌아가 과거에 폴리에게 코인을 사라고 말하고 싶네요;)
우선 한화로 50,000원을 매수한다고 가정해보겠습니다.
이 돈으로 매수한 코인은 0.00099168฿ 밖에 안되지만... 저에겐 아주 소중합니다.
@Test
public void bitCoinTest(){
double 오만원으로_산_비트코인_개수 = 0.00099168; // 현 시세
double 열아홉배_수익_가즈아 = 오만원으로_산_비트코인_개수 * 19;
double 비트코인_떡락이_두려워_판매한_개수 = 오만원으로_산_비트코인_개수 * 18;
double 남은_비트코인_개수 = 열아홉배_수익_가즈아 - 비트코인_떡락이_두려워_판매한_개수;
Assertions.assertThat(남은_비트코인_개수).isEqualTo(오만원으로_산_비트코인_개수);
}
/*
org.opentest4j.AssertionFailedError:
expected: 9.9168E-4
but was: 9.916799999999983E-4
Expected :9.9168E-4
Actual :9.916799999999983E-4
*/
위 코드를 보면 증가시킨 만큼 뺏기 때문에 원래 값으로 돌아와야하지만, 아주 작은 오차를 발생시킵니다.
0.00099168(9.9168E-4) <- 계산 전
0.0009916799999999983(9.916799999999983E-4) <-계산 후
double(가수부 52bit)의 percision는 float(가수부 23bit)보다 정밀하다해도 이렇게 미묘한 값의 오차를 만드는 것은 다름 없습니다.
이는 실수의 표현방식에서 오는 한계이기 때문에 32비트던, 4비트던 오차를 만들 수 밖에 없습니다.
숫자에 민감한 분야는 당연히 float은 사용하면 안되겠고, double을 사용하더라도 rounding rule이 필요할 것으로 생각됩니다.
다른 방법으로는 BigDecimal Class를 사용하는 것이 있습니다.
위와 같은 정밀도 문제에 대응할 수 있는 java.math 패키지 밑의 클래스 입니다.
Java 문서를 보면 다음과 같이 설명되어있습니다.
A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.
위의 내용으로 보면 BigDecimal은 2가지 파트로 구성되어 있음을 알 수 있습니다.
1) arbitrary precision integer unscaled value
2) 32-bit integer scale.
1)을 해석하자면 "임의의 정밀도를 가지는 정수값"정도가 됩니다.
쉽게 말하면 "임의의 길이를 가지는 정수"라 보면 될 것입니다.
2)는 32bit 정수값으로 된 "소수점이 찍히는 위치"정도로 이해하시면 되겠습니다.
말로만 표현하자니 햇갈릴 것 같아 수식과 예시를 보이겠습니다.
이에 대해서 예로 보자면 다음과 같습니다.
4.904894823467456456345 = 4904894823467456456345 * 10^-21
// 정수 * 10^-왼쪽부터 이동한 소수점의 칸 수
@Test
@DisplayName("두 BigDecimal 객체를 비교합니다.")
public void towBigDecimalEqualsTest(){
BigDecimal bigDecimalMadeOfFloat = new BigDecimal(0.1289371f);
BigDecimal bigDecimalMadeOfString = new BigDecimal("0.1289371");
Assertions.assertThat(bigDecimalMadeOfFloat).isEqualTo(bigDecimalMadeOfString);;
}
/*
org.opentest4j.AssertionFailedError:
expected: 0.1289371
but was: 0.1289370954036712646484375
Expected :0.1289371
Actual :0.1289370954036712646484375
*/
이에 대한 자세한 동작은 다음에 살펴보도록하겠습니다
Author And Source
이 문제에 관하여(실수의 표현방식 - 부동소수점(IEEE 754)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@indongcha/실수의-표현방식-부동소수점IEEE-754저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)