☕️ java

[Java] 실수 표현 - 고정소수점, 부동소수점

beomsic 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에서는 두 가지 방법을 제공

  1. int, long 정수형 타입으로 치환
  2. 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