-
[Java] 실수 표현 - 고정소수점, 부동소수점☕️ java 2023. 3. 7. 13:40
📌 실수의 메모리 표현
컴퓨터 메모리는 2진수 체계를 기반으로 데이터를 저장한다.
- 실수 역시 2진수 메모리 비트로 표현해야 한다 → 정수에 비해 복잡
컴퓨터에서 실수를 표현하는 방식으로는 대표적으로 고정 소수점 방식, 부동 소수점 방식으로 나눌 수 있다.
🔍 고정 소수점 방식
💡 고정 소수점 방식 (Fixed-Point Number Representation)
- 메모리를 정수부와 소수부로 고정으로 나누고 지정하여 처리하는 방식
- 부호 비트 - 양수 / 음수를 표현하기 위한 비트
- 0 : 양수
- 1 : 음수
예시
5.625
= 4 + 1 + 0.5 + 0.125
= 2^2 + 2^0 + 2^{-1} + 2^{-3}
= 101.101(2)
- 이진수 실수 계산 결괏값을 각각 정수부, 소수부 메모리 비트에 넣는다.
장 / 단점
장점
- 메모리에 실수 표현을 직관적으로 할 수 있다.
단점
- 표현 가능한 범위가 매우 적다. (치명적인 단점)
- 낭비되는 공간이 많이 생긴다.
📖 표현 가능한 범위가 적다.
Java의 float 타입을 기준으로 실수 메모리는 총 32 bit를 할당받는다.
- 고정 소수점 방식으로 메모리를 반으로 나누어 설계하면
- 정수부 비트에서 최대로 표현할 수 있는 숫자는 2^{15} - 1 인 32767이 된다.
- 40000.01 이라는 실수가 있다면 표현 범위를 넘어서 메모리에 적재할 수 가 없다.
📖 낭비되는 공간이 많이 생긴다.
- 2000.12 라는 실수가 있을 경우 0.12 라는 작은 숫자를 표현하기 위해 16비트의 소수부를 모두 사용하게 된다.
이런 공간 낭비를 줄이고 효율적으로 실수 메모리를 표현하기 위해서 컴퓨터는 부동 소수점 방식을 사용한다.
⭐ 부동 소수점 방식
💡 부동 소수점(floating point) 방식
- 소수점이 둥둥 떠다닌다 (floating) 라는 의미,
- 고정 소수점 방식과 달리 메모리를 가수부(23bit), 지수부(8bit)로 나눈다.
- 가수부 - 실제 실수 데이터 비트들이 들어간다.
- 지수부 - 소수점의 위치를 가리키는 제곱승이 들어간다.
🆚 고정 소수점 방식
고정 소수점
- 12.345 ( 정수 . 소수 )
부동 소수점
- 1.xxx X $2^n$ ( 가수 x 지수 )
부동 소수점은 직관적이지 않다.
하지만 이런 식으로 실수를 표현하는 이유는 큰 범위의 값을 표현하기 위해서이다.
고정 소수점 방식은 물리적으로 정수부와 소수부로 나누어 각각 15, 16bit만 사용할 수 있었다.
하지만, 부동 소수점 방식은 실수의 값 자체를 가수부(23bit)에 넣어 표현한다
⇒ 보다 큰 비트의 범위를 가지게 된다.
또한, 정수부가 크든 소수부가 크든 가수부 내에서 전체 실수를 표현한다.
⇒ 공간 낭비 문제를 해결할 수 있다.
📖 지수(e) 표기법
- 지수 표기법이란 아주 큰 숫자나 아주 작은 숫자를 간단하게 표기할 때 사용되는 표기법이다.
- 긴 실수를 나타내는데 필요한 자릿수를 줄여 표현해 준다는 장점이 있다.
double var1 = 1.234e2; // 1.234 * 10^2 = 123.4 double var2 = 1.7e+3; // 1700.0 double var3 = 1.7e-3; // 0.0017
📌 프로그래밍에서 소수 계산 오차 발생
컴퓨터에서 실수를 저장하는 방식에는 고정 소수점 방식과 부동 소수점 방식이 있다.
- 부동 소수점 방식이 좀 더 큰 수를 표현할 수 있다.
무한 소수가 있다면 이는 부동 소수점 방식이라 해도 정확하게 저장할 수 없다.
- 메모리 한계까지 소수점을 넣고 반올림을 해준다.
⚠️ 컴퓨터의 메모리는 한정적이라 실수의 소수점을 표현할 수 있는 제한이 존재한다.
- 부정확한 실수의 계산값을 초래한다.
예시
@Test void test() { double value1 = 12.23; double value2 = 34.59; assertThat(value1 + value2).isEqualTo(46.82); }
결과
expected: 46.82 but was: 46.82000000000001
- 기대결과인 46.82 가 나오지 않고 실제로는 46.82000000000001 가 출력된다.
Why❓
- 12.23 과 34.59 를 2진수로 변환하는 과정에서 소수점이 안 떨어지는 무한 소수 현상 발생!
- 따라서 메모리 할당 크기의 한계 때문에 특정 자릿수까지의 반올림 표현밖에 하지 못한다.
- 부정확한 값을 이용해 연산을 하게 된다.
- 따라서 값이 부정확하게 나온다.
❗이런 컴퓨터의 실수 연산 문제를 해결하기 위해 Java에서는 두 가지 방법을 제공
- int, long 정수형 타입으로 치환
- BigDecimal 클래스를 이용
📌 소수 정확히 계산하는 방법
정수 치환하여 계산하기
ex) 12.23 에 100을 곱해 1223 로 정수로 치환 후 계산 → 다시 100을 나누어 소수 결괏값을 도출
@Test void test() { double value1 = 1000.0; double value2 = 999.9; assertThat(value1 - value2 == 0.1).isFalse(); long v1 = (int) (value1 * 10); long v2 = (int) (value2 * 10); double result = v1 - v2; assertThat(result / 10.0).isEqualTo(0.1); }
BigDecimal 클래스
소수의 크기가 9자리를 넘지 않으면 int 타입을 사용하고,
18자리를 넘지 않으면 long 타입을 사용하면 된다.
하지만, 18자리를 초과하면 BigDecimal 클래스를 사용해야 한다.
📌 double 과 float
float는 4바이트 실수, double는 8바이트 실수 값을 저장할 수 있다.
컴퓨터에서 부동 소수점으로 실수를 표현하기 때문에 double과 float 간 정확도 차이가 발생한다.
@Test void test() { assertThat(1.0 == 1.0f).isTrue(); // 결과 : true assertThat(1.1 == 1.1f).isFalse(); // 결과 : false assertThat(0.1 == 0.1f).isFalse(); // 결과 : false assertThat(0.9 == 0.9f).isFalse(); // 결과 : false assertThat(0.01 == 0.01f).isFalse(); // 결과 : false }
❗ 따라서, double과 float 값을 비교할 때에는 모두 float로 형변환을 하거나 정수로 변환하여 비교해야 한다.
@Test void test() { assertThat((float) 1.1 == 1.1f).isTrue(); // 결과 : true assertThat(0.1f == (double) 0.1f).isTrue(); // 결과 : true assertThat(0.1 == (double) 0.1f).isFalse(); // 결과 : false }
- (double) 0.1f ⇒ double 공간에 float의 정밀도를 갖는 값이 저장되는 것.
- 따라서 double 형의 0.1 과 비교했을 때 같지 않다.
참고자료
https://inpa.tistory.com/entry/JAVA-☕-실수-표현부동-소수점-원리-한눈에-이해하기
https://jminc00.tistory.com/m/18
'☕️ java' 카테고리의 다른 글
🆚 Random 함수 비교 (0) 2023.10.23 DAO DTO / VO (0) 2023.03.10 BigDecimal ❓ (0) 2023.03.07 [Java] String / StringBuffer / StringBuilde (0) 2023.03.03 [Java] Static 과 Final (0) 2023.03.03