본문 바로가기
프로그래밍 공부/C++ 프로그래밍

[C++ 기초] 부동소수점 표기법(Floating-Point Arithmetic)

by 섬댕이 2023. 5. 23.

 

부동소수점 표기법(floating-point arithmetic)

부동소수점 표기법이란, 실수(real number)를 고정 정밀도(precision)를 가지는 유효숫자(significand)와 어떠한 밑(base)의 거듭제곱과 곱한 형태로 근사적으로 나타내는 표기법이다. 이때 밑은 일반적으로 진법(numeral system)에 따라 정한다.

 

  • 고정소수점 표기법: \(12.345\)
  • 부동소수점 표기법: \(12345 \times 10^{-3}\), \(1.2345 \times 10^{1}\) 등 여러 가지로 표현 가능

 

위의 예시에서와 같이 고정소수점 표기법으로 12.345로 나타내어지는 실수는 부동소수점 방식으로 표현할 때 이론상 무수히 많은 경우의 수로 나타내어질 수 있다. 이처럼 같은 수여도 지수의 값을 어떻게 정하냐에 따라 소수점의 위치가 계속 바뀌어 표기될 수 있기 때문에 부동(浮動, '떠다닌다 = floating'의 의미)소수점 이라는 이름이 붙었다. 컴퓨터 과학 분야에서 이러한 부동소수점 표기가 사용되는 이유는, 부동소수점 표기를 이용하면 고정소수점 방식으로 수를 표현할 때와 비교하여 동일한 메모리 크기로 더 넓은 표현범위를 나타낼 수 있기 때문이다. 단, 부동소수점 표기법은 근사적 표현이므로 오차 범위를 가진다는 특징이 있다.

 

 


 

정규화된(normalized) 부동소수점 수

부동소수점 수는 일반적으로 \(N\)-진법에 대하여 모두 표현 가능하나, 컴퓨터는 이진법에 기반하므로 이 포스팅에서는 이진법을 기준으로 하는 경우(즉, 밑을 2로 하는 경우)의 부동소수점 수에 대해서만 논한다.

 

임의의 실수 \(x\)는 항상 다음과 같이 부동소수점 표기법으로 나타낼 수 있다.

$$x = \pm\beta \times 2^{e}$$

이때, 유효숫자를 나타내는 가수부(matissa, fraction) \(\beta = (.b_{1}b_{2} \cdots b_{k})\)에 대하여, \(b_{1} = b_{2} = \cdots = b_{k} = 0\)(즉, \(\beta = 0\)인 경우) 또는 $b_{1} \neq 0$인 경우(즉, $1 \leq \beta < 2$인 경우)에는 부동소수점 수 $x$정규화되었다고 표현한다.

 

아래에서 자세히 알아보겠지만 0을 나타낼 때는 지수부(exponent)의 비트를 이용해 예외적으로 표현하므로, 항상 \(b_{1} = 1\)이라고 가정할 수 있다. 따라서 컴퓨터에서 부동소수점 수를 저장할 때에는 가수부 $\beta = 1.b_{2} b_{3} \cdots b_{k}$에 대해 정수 부분 1을 제외한 소수점 아래 부분인 $0.b_{2} b_{3} \cdots b_{k}$만을 가수부 비트에 저장한다.

 

 


 

IEEE 표준 부동소수점 표기 / 저장 방식

IEEE 표준에 따르는 부동소수점 수 저장 방식은 메모리 공간을 아래와 같이 나누어 값을 저장한다.

 

출처: 위키피디아 (https://ko.wikipedia.org/wiki/부동소수점)

* 최상위비트는 정밀도에 관계 없이 항상 1비트로 고정(0 = 양수, 1 = 음수).

 

  • 부호를 나타내는 값을 최상위 비트(most significant bit, MSB)에 저장.
  • 지수 $e$를 지수부 비트에 저장.
  • 정규화 된 가수부 $\beta$를 가수부 비트에 저장.

 

 


 

가수부(matissa, fraction)의 표현, 유효숫자 자릿수

IEEE 표준(IEEE 754)에서의 부동소수점 정밀도(precision)는 유효숫자를 표현하는 가수부의 비트 수에 의해 결정된다. 가수부의 비트 수가 \(k\)비트이면 0부터 $2^{k} - 1$ 까지의 수를 표현 가능(부호는 최상위비트로 표현)하므로 이 값의 자리수에 따라 유효숫자의 자릿수가 정해진다.

 

  • 단정밀도(single-precision): 지수부 8비트, 가수부 23비트 $\Longrightarrow$ 유효숫자: 6-7자리
  • 배정밀도(double-precision): 지수부 11비트, 가수부 52비트 $\Longrightarrow$ 유효숫자: 15-16자리

* 참고1) $2^{23} = 8388608$ 이 7자리의 수이므로 23비트로 6자리의 수까지는 항상 정확한 표현이 가능.

* 참고2) $2^{52} = 4503599627370496$ 이 16자리의 수이므로 52비트로 15자리의 수까지는 항상 정확한 표현이 가능.

 

이외에도 IEEE 표준은 하드웨어에서 표현할 수 있는 부동소수점 형식으로 반정밀도, 4배정밀도, 확장배정밀도(long double) 형식도 지정한다. long double 형식의 경우에는 특정 C/C++ 컴파일러만 지원하며 Microsoft C++ 기준으로는 long double 형식도 double의 스토리지 형식에 매핑이 된다. 하드웨어에서 지원되는 다른 형식들의 경우에는 계산을 위한 내장 함수 또는 어셈블리 언어를 통해 지원된다고 한다(관련 링크).

 

 


 

지수부(exponent)의 표현, 바이어스(bias) 표기법

표현하고자 하는 수에 부호가 존재하는 것처럼, 지수부 역시 음수와 양수 모두 존재할 수 있다. 편의상 지수부의 부호는 최상위비트를 사용하는 방식으로 표현하지 않고 바이어스(bias) 표기법이라는 방식을 사용하는데, 이는 지수부의 가능한 값(예외 처리를 위한 00...0, 11....1의 2 가지 표현을 제외한 나머지)의 절반을 의미한다. 바이어스 표기법에 따라 지수 값이 지수부에 저장될 때는 실제의 지수 값에 바이어스 값을 더한 결과가 비트화되어 unsigned 형태로 저장된다.

 

이렇게 바이어스 표현법에 따라 지수부를 표현하는 이유는

 

  • 지수부의 부호의 의미는 숫자 자체의 부호의 의미와 다름(절댓값이 큰 음수 지수는 0에 가까운 수를 나타냄),
  • 지수의 계산을 더 용이하게 수행하기 위해 부호가 없는 수인 것처럼 표현하기 위함

 

등의 이유 때문이다. IEEE 표준에서 정밀도에 따른 바이어스 값과 지수 표현 범위는 아래와 같다.

 

  바이어스 지수 표현 범위
단정밀도 $(2^{8} - 2) / 2 = 2^{7} - 1 = 127.$ $-126 \leq e \leq 127$
배정밀도 $(2^{11} - 2) / 2 = 2^{10} - 1 = 1023.$ $-1022 \leq e \leq 1023$

 

지수부의 모든 비트가 0이거나 1인 경우를 제외하는 이유는 해당 값이 아래와 같은 예외적인 상황을 나타내기 위해 사용되기 때문이다.

 

  • 0: 지수부의 모든 비트가 0이며, 가수부의 모든 비트가 0인 경우. -0과 0은 같은 0으로 취급한다.
  • 비정규화 된 부동소수점 수: 지수부의 모든 비트가 0이며, 가수부의 비트 중 0이 아닌 비트가 존재하는 경우.
  • $\pm$inf: 지수부의 모든 비트가 1이며, 가수부의 모든 비트가 0인 경우. 부호는 최상위비트를 따른다.
  • NaN(0/0과 같이 실수가 아닌 값): 지수부의 모든 비트가 1이며, 가수부의 비트 중 0이 아닌 비트가 존재하는 경우.

* 비정규화 된 부동소수점 수는 표현 범위의 최소값보다 0에 더 가까운 값을 나타내기 위해 사용한다. 비정규화 된 부동소수점 수의 경우는 가수부의 정수 부분이 1이 아닌 0이라고 간주하며 지수부는 $1 - (bias)$ 로 예약된다.

 


 

float, double 자료형의 표현 범위

C/C++에서 부동소수점 방식으로 실수를 표현하는 기본 자료형인 float, double은 각각 단정밀도, 배정밀도로 부동소수점 수를 나타내는 형식이다. 두 자료형에 대해 표현 가능한 값의 최소, 최대를 살펴보면(편의상 0보다 큰 양수일 때를 기준)

 

  • 최대: 정규화 된 가수부 비트가 1111...1($\approx 2)$이며, 지수부 비트가 111...10일 때
  • 최소: 정규화 된 가수부 비트가 0000...0($= 1$)이며, 지수부 비트가 000...01일 때

 

이므로, 0보다 큰 양수에 대한 표현 범위는

 

  최소 최대
float 자료형 $2^{-126} =$ 1.17549435E-38 $2 \times 2^{127} \approx$ 3.40282367E+38
double 자료형 $2^{-1022} =$ 2.22507386E-308 $2 \times 2^{1023} \approx$ 1.79769313E+308

 

* 참고) 표현하고자 하는 수가 부동소수점 형식의 표현 범위 내에 있으면서 2의 거듭제곱인 경우는 특수한 경우로, 표현 가능한 유효숫자 자릿수와 관계없이 모든 자리의 숫자를 정확하게 저장할 수 있다. 그 이유는 2의 거듭제곱인 수의 가수부는 1이므로 근사값이 아닌 정확한 값으로써 저장되기 때문이다. 물론, 추가적인 연산이 수행되는 경우에는 바로 오차가 발생하여 상위 자릿수부터 유효숫자 자릿수에 포함되지 않는 범위의 자릿수 값들은 전부 부정확해진다.

 

이를 직접 확인해보기 위해서 임시로 코드를 짜보았다. 아래의 코드에서 MultiplyN 함수는 숫자를 string으로 표현하여, int 범위 내의 수를 곱하는 함수이다(double을 초과하는 범위의 수에 대해서도 계산결과를 확인하도록 직접 짠 코드). 아래의 코드를 직접 실행해보면서 base, exponent 변수에 초기화 되는 값을 바꿔 확인해볼 수 있다.

* 임시로 짠 코드라서 비효율적인 연산을 수행하는 코드일 수 있음.

// 거듭제곱 형태의 큰 숫자 출력해보기
// base, exponent 값을 변경하면서 확인해보기

#include <iostream>
#include <vector>
#include <string>

using namespace std;

void MultiplyN(string& largeNum, int n)
{
	bool negative = false;
	if (largeNum[0] == '-')
	{
		negative = true;
		largeNum.erase(largeNum.begin());
	}

	vector<int> result;
	for (int i = 0; i < (int)log10(abs(n)) + 2; i++)
		result.push_back(0);

	for (char c : largeNum)
		result.push_back((c - '0') * abs(n));

	for (size_t i = result.size() - 1; i > 1; --i)
	{
		if (result[i])
		{
			result[i - 1] += result[i] / 10;
			result[i] = result[i] % 10;
		}
	}

	while (!result[0])
		result.erase(result.begin());

	if (negative)
		largeNum = n > 0 ? "-" : "";
	else
		largeNum = n > 0 ? "" : "-";

	for (int c : result)
		largeNum += to_string(c);
}

int main()
{
	int base = 2;
	int exponent = 6;

	string pow2_to_string = to_string(pow(base, exponent));
	cout << "1) to_string, pow 함수 이용: \n";
	cout << pow2_to_string << "\n\n";

	double pow2_double = 1.0;
	for (int i = 0; i < exponent; i++)
		pow2_double *= base;
	printf("2) double 형 이용:\n%lf\n\n", pow2_double);

	string pow2_string = "1";
	for (int i = 0; i < exponent; i++)
		MultiplyN(pow2_string, base);
	cout << "3) string 클래스를 이용한 계산 결과(base, exponent가 int 범위 내이면 정확):" << "\n";
	cout << pow2_string << "\n";
}

 


 

* 해당 카테고리의 글은 Microsoft 공식 홈페이지의 기술 문서 페이지와 Stephan Prata의 저서 C++ 기초 플러스 (6판, 번역본) 서적 및 기타 구글링을 통한 정보 수집 등을 토대로 개인적으로 요약/정리해 본 글입니다. 만약 잘못 정리된 내용이 있다면, 댓글이나 이메일로 공유해주시면 수정하도록 하겠습니다.

댓글