Post

고정 소수점 (Fixed Point) / 부동 소수점 (Floating Point)

이 글은 제 개인적인 공부를 위해 작성한 글입니다. 틀린 내용이 있을 수 있고, 피드백은 환영합니다.

고정 소수점 (Fixed Point) / 부동 소수점 (Floating Point)

개요


컴퓨터는 모든 데이터를 0과 1로 기억한다. 정수는 2진수로 쉽게 바꿀 수 있지만, 3.14나 0.001과 같은 실수(float)는 어떻게 저장할까? 실수를 표현하기 위해 크게 고정 소수점(Fixed Point)부동 소수점(Floating Point)이라는 두 가지 방식을 사용한다.


고정 소수점 (Fixed Point)


소수점의 위치를 딱 고정해 두고 쓰는 방식

말 그대로 소수점이 위치할 자리를 미리 정해놓고, 남은 비트들을 정수부와 소수부로 나누어 할당하는 방식이다. 예를 들어 32비트 시스템이라면, 2진수로 표현하자면 아래와 같이 표현할 수 있다.

[0 000000000000000 0000000000000000 ]

  • 검정색 부분 (1bit) : 부호를 결정. 0이면 양수, 1이면 음수
  • 빨간색 부분 (15bit) : 정수를 표현
  • 초록색 부분 (16bit) : 소수점 이하 자릿수를 표현

예시로 9.6875를 고정 소수점으로 표현해보자.

  • 검정색 부분 : 0 (양수)
  • 빨간색 부분 : 9를 2진수로 표현하면 …1001
  • 초록색 부분 : 0.6875를 2진수로 표현하면 1011…

즉, [0 000000000000001001 1011000000000000 ]

이런 식으로 32비트가 구성된다.


정수를 이진수로 표현하면 아래와 같았다면,

  • 0001 = 1
  • 0010 = 2
  • 0100 = 4
  • 1000 = 8

2진수로 소수점 이하에서는 2를 나눠서 표현한다.

  • 0.1 = 1/2 = 0.5
  • 0.01 = 1/4 = 0.25
  • 0.001 = 1/8 = 0.125
  • 0.0001 = 1/16 = 0.0625

따라서 1011000000000000은 0.5 + 0.125 + 0.0625 = 0.6875가 된다.

고정 소수점 방식은 정수 부분과 소수 부분의 자릿수가 적어서 표현할 수 있는 범위가 적다는 단점이 있다.


부동 소수점 (Floating Point)


부동(浮動)이란 뜰 부(浮)에 움직일 동(動)을 써서 떠서 움직인다는 뜻이다. 움직이지 않는다는 뜻의 부동도 있기에 헷갈릴 수 있다!

즉, 소수점이 고정되어 있지 않고 좌우로 움직일 수 있다는 뜻이다. 고정 소수점과는 달리 소수점을 움직일 수 있어 표현할 수 있는 수의 범위가 매우 넓다는 장점이 있다.

부동 소수점을 표현하기 위한 수식은 아래와 같다. \((-1)^s \times (1 + f) \times 2^{(e - bias)}\)

[0 00000000 00000000000000000000000 ]

  • 검정색 부분 (1bit, s[Sign]) : 부호를 결정. 0이면 양수, 1이면 음수
  • 빨간색 부분 (8bit, e[Exponent]) : 지수부. 소수점의 위치를 좌우로 움직이는 지수이다. 양수와 음수를 표현해야 하기에 [-127 ~ 128]까지 표현하는데, 바이어스(Bias) 방식을 사용한다.
  • 초록색 부분 (23bit, f[Fraction]) : 가수부. 소수점 오른쪽 실제 유효 숫자를 나타낸다. 2진수 정규화를 거치면 항상 1.xxx... 형태가 되기에, 맨 앞의 1.은 생략하고 소수점 아래 xxx... 부분만 메모리에 저장한다.

이해를 돕기 위해 10진수로 된 실수의 정규화(표준 표기법)을 알아보면, 가장 큰 자리수의 숫자에 소수점을 부여하고 진짜 소수점의 위치는 10의 지수로 표현한다. 예를 들어, 123.45는 1.2345 x 10^2로 표현할 수 있다.

부동소수점을 이용하여 2진수로 실수를 정규화할 때도 마찬가지이다. 9.6875를 부동소수점으로 표현한다면 정수부와 소수부를 나눠서 1001.1011이 되고, 이를 정규화하면 1001.1011 -> 1.0011011 x 2^3이 된다.

이걸 2진수로 표현하면 아래와 같다. [ 0 10000010 00110110000000000000000 ]

부호(검정색), 지수부(빨간색), 가수부(초록색)를 각각 살펴보면, 먼저 부호는 양수니까 0이 맞다.


지수부는 3을 표현하려면 [00000011]이 되어야 하지만, [10000010]이다. 왜 그럴까? 지수부 8비트는 $2^{-126}$같은 음수 지수부터 $2^{127}$같은 양수 지수까지 모두 표현해야 한다. 그런데 컴퓨터는 음수 부호를 쓰면 크기 비교가 느려지니까 꼼수를 부린 것이다.

“모든 지수에 127을 더해서 전부 양수로 만들어버리자”라는 바이어스(Bias) 방식을 사용한다. 그래서 지수가 0일 때는 0 + 127 = 127[01111111]이 되고, 우리가 구한 지수 3은 3 + 127 = 130[10000010]이 된다.


다음은 기수부를 표현하는 초록색 부분을 보자.

원래는 100110110000000000000000처럼 되어야 하는데 맨 왼쪽의 1이 빠져있다. 이유는 가장 왼쪽의 1은 무조건 1이 되므로 굳이 기록하지 않는 것이다.


다시 정리하면 [ 0 10000010 00110110000000000000000 ]에서

기수부는 [0011011] -> [1.0011011] (맨 앞에 1과 소수점 추가)

지수부는 3이니 [1.0011011] -> [1001.1011]

이것을 10진수로 표현하면 9.6875가 되는 것이다.


부동 소수점의 한계


  1. 소수점 이하 데이터 유실

    C++에서 부동 소수점(float, double)을 정수형(int)로 변환할 때는 반올림이 아니라 소수점 아래를 무조건 버리는 ‘버림(Truncation)’ 방식이 적용된다.

    1
    2
    3
    4
    5
    
     float v1 = 3.99f;
     std::cout << v1 << '\n'; // OUTPUT : 3
        
     float v2 = -3.99f;
     std::cout << v2 << '\n'; // OUTPUT : -3
    


  2. 정밀도 한계로 인한 근사치 오차 (Precision Loss)

    컴퓨터는 10진수 소수점을 2진수로 정확히 표현하지 못하는 경우가 많다. 어떤 수가 무산 소수가 되어 유실되는 비트가 생길 수 있고, 이 미세한 오차가 정수 변환이나 대소 비교 시 예상하지 못한 결과를 낼 수 있다.

    1
    2
    
     double v3 = 0.58 * 100; // 수학적으로는 정확히 58이지만, 실제로는 57.999...
     std::cout << static_cast<int>(v3) << '\n'; // OUTPUT : 57 
    
  3. 정수 오버플로우와 정의되지 않은 동작

    부동 소수점 타입이 가진 값의 범위는 int보다 훨씬 크다. int가 표현할 수 있는 최대값이나 최소값보다 크거나 작은 부동 소수점을 강제로 정수로 변환하려고 하면 정의되지 않은 동작이 발생한다.

    1
    2
    
     double v4 = 5000000000.0; // 50억
     std::cout << static_cast<int>(v4); // OUTPUT : 쓰레기 값
    

이러한 문제를 해결하기 위해서, 정확한 반올림/올림/내림이 필요하다면 헤더의 std::round(), std::ceil(), std::floor()를 명시적으로 사용하자. 그리고 정수 변환 시 `static_cast(value + 0.5f)` 같은 방식으로 반올림 기법을 사용해도 좋다. 동등 비교 시에는 `std::abs(a - b) < EPSILON` 처럼 두 값의 차이가 매우 작은 값보다 작은지 체크하는 방법도 있다.


참고

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