1. 개요
컴퓨터는 10진수가 아닌 2진수로 수를 표현한다. 이 문서는 일반적인 컴퓨터가 정수와 실수를 어떻게 표현하는지를 정리한 문서이다.2. 컴퓨터에서 정수 표현하기
일반적으로[1] 컴퓨터에서 사용되는 정수형의 종류는 다음과 같다.크기 | 바이트 | 부호 여부 | 범위 | 어셈블리 자료형[2] | C 언어 자료형[c] |
8비트 | 1바이트 | 없음 | [math(\left[0,\,2^8-1\right])] | BYTE | unsigned char |
있음 | [math(\left[-2^7,\,2^7-1\right])] | SBYTE | char | ||
16비트 | 2바이트 | 없음 | [math(\left[0,\,2^{16}-1\right])] | WORD | unsigned short |
있음 | [math(\left[-2^{15},\,2^{15}-1\right])] | SWORD | short | ||
32비트 | 4바이트 | 없음 | [math(\left[0,\,2^{32}-1\right])] | DWORD | unsigned |
있음 | [math(\left[-2^{31},\,2^{31}-1\right])] | SDWORD | int | ||
64비트 | 8바이트 | 없음 | [math(\left[0,\,2^{64}-1\right])] | QWORD | unsigned long 또는 unsigned long long [c] |
있음 | [math(\left[-2^{63},\,2^{63}-1\right])] | SQWORD | long 또는 long long [c] |
각 정수는 음수를 표현할 수 없고 양수 크기가 두 배로 지원되는
unsigned
형을 가진다.위 표의 크기는 64비트 유닉스/리눅스가 사용하는 LP64위의 AMD64어셈블리와 C 언어를 기준으로 한 것이다. 운영체제, CPU 아키텍처, 프로그래밍 언어에 따라 크기나 형의 이름이 다를 수 있다. 예를 들면 같은 윈도 시스템에서도 .NET Framework의
long
형은 64비트이다. 정수형의 크기가 중요한 프로그램을 개발한다면 C의 int32_t
와 같이 각 언어나 언어별 표준 라이브러리, 프레임워크에서 제공되는 크기가 명시적으로 표현된 자료형을 사용하도록 하자. 단, 이 경우 <stdint.h>
헤더를 선언해야 한다.또한 단일 크기 정수형을 지니거나(PHP 등), 정수형의 크기 제한이 없는 경우(Python 등)도 있다.
2.1. 메모리에 저장하는 방법
메모리에서는 정수 데이터를 저장하기 위해 4칸을 쓰게 된다. 바이트는 컴퓨터가 정보를 저장하는 가장 작은 단위이자 메모리 상에서 주소가 배정될 수 있는(addressable) 가장 작은 단위이다. 메모리는 바이트 단위로 주소가 배정되어 있고[6], 정수는 4바이트이므로 4칸이 필요하다. 당연하지만 64비트 수 체계는 이의 2배인 8칸을 차지한다.2.1.1. 빅 엔디언, 리틀 엔디언
1바이트로 다 담을 수 없는 정수를 메모리에 어떤 순서로 저장하냐에 따라 Big Endian, Little Endian, Bi Endian으로 분류할 수 있다.자세한 내용은 엔디언 문서 참고하십시오.
2.2. 표현법
2.2.1. (signed)음수를 표현
총 32개의 비트 중 첫 번째 비트(가장 왼쪽에 위치한 비트)를 부호 표현을 위해 따로 배정한다. 이를 부호 비트(signed bit)라 부른다. 부호 비트가 0이면 양수를, 1이면 음수를 나타낸다. 아래 소개된 음수 표현 방법들에서 음수들은 모두 1을 첫 번째 비트에 가진다.2.2.1.1. 양수를 표현할 때
부호 비트는 0으로 놓고, 남은 자리에 2진수를 그대로 표현하면 된다. 예를 들어,0100 0000 0000 0000 0000 0000 0000 0000
(2) (0x40000000) = 10737418240000 0100 1001 1000 0000 0000 0011 1111
(2) (0x0498003F) = 770703990000 0000 0000 0000 0000 0000 0000 1000
(2) (0x00000008) = 8이런 식이다.
따라서 표현할 수 있는 가장 큰 32비트 값은 0111 1111 1111 1111 1111 1111 1111 1111(2) (0x7FFFFFFF) = 2147483647 가 된다.
2.2.1.2. 음수를 표현할 때
논리 회로가 음수를 표현하는 방법은 여러 가지가 있다. 대표적으로 3가지를 꼽자면 '부호화 절대치[7] 방법(Sign Magnitude)', '1의 보수 방법(1's Complement)', '2의 보수 방법(2's Complement)'가 있다. 보수 방법은 컴퓨터에서 쓰이는 양수가 전체 자연수가 아니라 0부터 상한까지 자른 부분집합이라는 성질을 잘 이용한 것이다.2.2.1.2.1. 부호화 절대치
Sign-magnitude부호 비트를 제외한 수를 양수로 읽고, 마이너스를 붙이는 방법. 예를 들면 이진수 000011(2) = +3으로, 100011(2) = -3으로 인식하는 것이다.
이는 인간 입장에서 표기하기 직관적이고 곱셈이나 나눗셈을 할 때 매우 유리하지만, 음수의 덧셈이 양수의 뺄셈과 전혀 딴판이라는 어이없는 결과가 나오므로 이를 해결하기 위해 피연산자의 절댓값을 서로 비교하는 등 추가적인 연산을 필요로 한다는 단점이 있다.
예를 들어 000011(2) = 3과 100011(2) = -3을 이진수 계산으로 더하면 000011(2) + 100011(2) = 100110(2) = -6이 되는데, 이는 결과값으로 나와야 할 (+3) + (-3) = 0과는 다른 값이다.
대표적으로, Unix time에 사용되고 있다.
2.2.1.2.2. 1의 보수
1's Complement양수의 비트들을 반전시켜서 음수를 표현하는 방법. 즉 이진수 000011(2) = +3의 비트를 모두 반전시켜 111100(2)을 만들어 -3을 표현하는 방법이다.
1의 보수 방법의 정확한 정의는 다음과 같다:
총 n개의 비트로 정수를 표현할 때, 모든 n비트가 1로 이루어진 수 (2n - 1 = 111...11(2))에서 표현하고 싶은 음수의 절댓값을 뺀 수가 바로 1의 보수 방법으로 표현한 음수가 된다.
1의 보수 방법이라 부르는 이유가 여기에 있다. 결과적으로 어떤 음수를 1의 보수 방법으로 표현하고 싶다면 그 음수의 절대값의 모든 비트 숫자들을 반전시키면 된다.(0 은 1로 반전, 1은 0으로 반전) 이 성질 덕분에 1의 보수 방법에서는 2진수 연산값이 실제 값과 같다.[8]
하지만 0을 나타내는 값이 000...00(2)(모든 비트가 0인 수)과 111...11(2)(모든 비트가 1인 수) 두 가지나 생겨버린다. 상술한 1의 보수 방법의 정의대로 해석하면 모든 비트가 0인 수는 +0, 모든 비트가 1인 수는 -0을 의미하는데, 이는 -0은 +0을 1의 보수화(=비트 반전)한 결과이기 때문이다. 따라서 계산 결과에 따라 [math(+0\neq-0)]인 황당한 상황이 벌어질 수도 있다.
또한 덧셈 연산을 할 때 end around carry[9]가 발생해서 1을 더해야 할 때가 있다는 단점이 있다.
2.2.1.2.3. 2의 보수
2's Complement1의 보수 방법에 1을 더하는 방법.[10] 즉 이진수 000011(2) = +3의 비트를 모두 반전시켜 111100(2)을 만들고, 여기에 1을 더해 111101(2)로 -3을 표현하는 방법이다.
2의 보수 방법의 정확한 정의는 다음과 같다:
총 n개의 비트로 정수를 표현할 때, 2n = 1000...0(2)에서 표현하고 싶은 음수의 절댓값을 뺀 수가 바로 2의 보수 방법으로 표현한 음수가 된다.
2의 보수 방법이라 부르는 이유가 여기에 있다. 결과적으로 어떤 음수를 2의 보수 방법으로 표현하고 싶다면 그 음수의 절대값의 모든 비트 숫자들을 반전시킨 후에(0 은 1로 반전, 1은 0으로 반전) 1을 더하면 된다. 즉, 1의 보수 방법에서 1을 더하는 과정이 추가된 것과 같다.
2의 보수 방법에서는 1의 보수 방법에서와 다르게 111...11(2)이 의미하는 값이 -1을 의미하므로[11] 000...00(2) = 0과 구분된다. 1의 부호 방법과 비교하면 양수에 음의 부호를 붙일 때 1을 더해주는 추가 연산을 해야하긴 하지만 이것이 그것이 2의 보수 방법에서 감수해야 하는 유일한 불편함이고, 양수를 빼는(음수를 더하는) 연산을 할 때 다른 추가 작업(예를 들면 end around carry를 처리)을 해줄 필요가 없고 다른 방법들과 달리 +0과 -0을 구별 안 해도 되므로 장점이 더 많다. 따라서 컴퓨터에서는 2의 보수 방법을 이용한 뺄셈 연산을 주로 채용하고 있다.
컴퓨터는 보통 2의 보수 방법과 부호화 절대치 방법으로 음수를 표현한다. 정수나 고정 소수점에서 2의 보수를 주로 사용하고, 부동 소수점의 유효숫자는 부호화 절대치 방법을 사용한다. 그림에서 처럼 시계 방향으로 증가하는 형태로 값을 나열할 수 있다. 이것이 바로 2의 보수 숫자 포맷이고 숫자의 표현 범위는 [math(-2^{n-1})]에서 [math(2^{n-1}-1)]까지가 된다. 여기서는 [math(n=4)]이기 때문에 [math(-8)]에서 [math(7)]까지가 표현 범위다. 부호 절대값(Signed Magnitude)이나 1의 보수(1's Complement), 부호 숫자(Signed Digit), 음의 기수(Negative Radix) 등의 방법들은 특정 회로에서 유용함이 입증되어 하드웨어 레벨에서 내부적으로 사용하는 경우가 있다.[12]
signed int
의 범위는 다음과 같다.- 표현할 수 있는 수의 최솟값: 1000 0000 0000 0000 0000 0000 0000 0000(2) (0x80000000) = -2147483648
- 표현할 수 있는 수의 최댓값: 0111 1111 1111 1111 1111 1111 1111 1111(2) (0x7FFFFFFF) = 2147483647
- 이 두 수는 C언어의 limits.h 헤더 파일에 각각
INT_MIN
,INT_MAX
로 정의되어 있다.
2.2.2. (unsigned)음수를 표현 안함
컴퓨터에서는 정수를 표현할 때 경우에 따라서는 음수를 표현하지 않아도 될 때가 있다. 이 때는unsigned
선언을 해 주면 음수를 표현하지 않는 정수형(unsigned int
)를 쓸 수 있다. 이 경우 부호 비트까지도 값을 나타내는 데 쓰기에 표현할 수 있는 최대 정수 크기가 커진다. 물론 표현할 수 있는 가장 작은 정수가 커졌기에 int
가 표현할 수 있는 범위가 늘어나는 건 아니다.unsigned int
의 범위는 다음과 같다.- 표현할 수 있는 수의 최솟값: 0000 0000 0000 0000 0000 0000 0000 0000(2) (0x00000000) = 0
- 표현할 수 있는 수의 최댓값: 1111 1111 1111 1111 1111 1111 1111 1111(2) (0xFFFFFFFF) = 4294967295
- 최댓값은 C언어의 limits.h 헤더 파일에
UINT_MAX
로 정의되어 있다.
w비트의 정수가 표현할 수 있는 수의 범위는 다음과 같다.
타입 | 범위 |
unsigned int | 0 ~ (2w - 1) |
Signed - 부호 절댓값 방법 | -(2w - 1 - 1) ~ (2w - 1 - 1) |
Signed - 1의 보수 방법 | -(2w - 1 - 1) ~ (2w - 1 - 1) |
Signed - 2의 보수 방법(signed int ) | -2w - 1 ~ (2w - 1 - 1)[13] |
unsigned int에서는 4비트, 8비트, 32비트, 128비트 한정으로 [math((a+b)^n = a^n+b^n)]이 성립한다. 이는 메르센 소수에 속하는 조건이기 때문에 성립하는 것.[14]
2.2.3. (casting)형 변환
형 변환을 할 때의 규칙은 다음과 같다.- 각 비트의 숫자들은 그대로 유지한다.
- 각 비트를 해석(interpret)하는 방법을 다르게 한다.
예를 들어, 6비트의 signed 변수로 (2의 보수 방법으로) 표현된 -3 = 111101(2)을 unsigned로 해석해 61로 읽는 것이다.
형 변환은 명시적(explicit)으로 일으킬 수도 있지만, 묵시적(implicit)으로도 일어날 수 있다. 따라서 함부로
unsigned int
를 선언하는 것은 위험할 수 있다. 대부분의 시스템에서 기본적인 정수형은 signed int
이므로 데이터가 signed int
로 해석될 수도 있기 때문이다. 따라서 unsigned int
를 쓰는 상황은 단순히 음수 값을 가질 수 없는 상황에서보단 flag의 용도(계산을 하지 않는 용도)로 사용하는 것이 조금 더 알맞다 할 수 있겠다.2.3. 정수의 연산
컴퓨터 시스템에선 정수를 이용해 많은 연산을 수행한다. 여기선 컴퓨터가 정수를 가지고 연산을 할 때 일어나는 일들을 정리해 보았다. 이때, 이 컴퓨터는 w비트로 정수를 표현한다고 가정하자.2.3.1. Unsigned의 덧셈
두unsigned int
x, y를 더할 때, x + y가 가질 수 있는 범위는 [math(0 \le x + y \le 2^{w + 1} - 2)]이고, 이 범위는 w비트로 표현할 수 없다. 엄밀히 말하자면, 받아올림(carrying)이 발생하여 w비트로는 표현할 수 없게 된다. 이때, 컴퓨터는 가장 아래쪽 w비트만을 출력한다. 즉, 값 올림을 그대로 무시한다(버린다, truncate). 이렇게 계산 결과가 받아올림으로 표현의 범위를 초과해 잘못된 계산 결과를 출력하는 현상을 오버플로(overflow)라 한다. 이 오류는 구조적인 문제이므로 근본적인 디버그가 불가능하다. 이를 방지하려면 무작정 비트 수를 늘리는 방법 밖에는 없다. 8비트는 28-1=255, 16비트는 216-1=65535, 32비트는 232-1=4294967295를 넘길 경우 발생하지만, 64비트 264-1=18446744073709551615 정도면 일부러 오버플로를 내려 하지 않는 이상 거의 안 난다고 보는 것이 맞을 것이다.예를 들어, w = 4인 시스템에서 9 + 12를 계산한다면
- 9 + 12 = 1001(2) + 1100(2) = 10101(2)
2.3.2. signed의 덧셈
두signed int
x, y를 더할 때, x + y가 가질 수 있는 범위는 [math(-2^w \le x + y \le 2^w - 2)] 이고, 이 범위는 w비트로 표현할 수 없다. 이 때, unsigned의 덧셈과 마찬가지로 컴퓨터는 아래쪽 w비트만을 출력한다. 이 과정에서 원래의 부호 비트는 버려지면서 계산값의 부호가 바뀐다! 음수를 더했더니 양수가 나오는 예를 들어, w = 4인 시스템에서
- (-8) + (-5) = 1000(2) + 1011(2) (= 10011(2)) = 0011(2) = 3. 이렇게 음의 정수 두 개를 더해서 양수가 나오면 이를 음의 오버플로(Negative overflow)라 부른다.
- 5 + 5 = 0101(2) + 0101(2) = 1010(2) = -6.이렇게 양의 정수 두 개를 더해서 음수가 나오면 이를 양의 오버플로(Positive overflow)라 부른다.
2.3.3. signed의 음수화(negation)
signed int
x의 범위는 [math(-2^{w-1} \le x \le 2^{w-1} - 1)]이므로 -x의 범위는 [math(-2^{w-1} + 1 \le -x \le 2^{w-1})]이다. 이 때, x = -2w-1이면 -x를 w비트의 수로 표현할 수 없다는 것을 관찰할 수 있다. 이 경우 컴퓨터는 -x = -2w-1로 처리한다.예를 들어, w = 4인 시스템에서
x | -x |
-4 (1100(2)) | 4 (0100(2)) |
-8 (1000(2)) | -8 (1000(2)) |
5 (0101(2)) | -5 (1011(2)) |
7 (0111(2)) | -7 (1001(2)) |
2.3.4. unsigned, signed의 곱셈
위와 마찬가지로 오버플로에 대해 아래쪽 w비트의 결과만을 인식한다.예를 들어, w = 3인 시스템에서
x | y | xy (이론상의 값) | xy (truncated) | |
Unsigned | 5 (101(2)) | 3 (011(2)) | 15 (1111(2)) | 7 (111(2)) |
Signed | -3 (101(2)) | 3 (011(2)) | -9 (110111(2)) | -1 (111(2)) |
예를 들어 14x를 계산해야 한다면, 컴파일러는 이를 다음과 같이 최적화한다.
- 14x = (1110(2))x
- = (23 + 22 + 21)x
- = 23x + 22x + 21x
- =
(x << 3) + (x << 2) + (x << 1)
- 14x = (1110(2))x
- = (10000(2) - 10(2))x
- = (24 - 21)x
- = 24x - 21x
- =
(x << 4) - (x << 1)
3. 컴퓨터에서 유리수 표현하기
하나의 데이터로 정의하려고 하면 할 수는 있지만 상위 집합인 실수와 마찬가지 문제로, 실제로 구현하는 경우는 거의 없다. 실수든 유리수든 컴퓨터에서 쓰는 경우는 대부분 특정한 계산 결과의 참값을 표시하는 것이 아닌, 애초에 근사된 데이터를 그대로 표현해야 하는 경우가 많아 굳이 유리수를 하나의 '비트 데이터'로 표현해야 할 이유가 없기 때문.따라서 컴퓨터에서 유리수의 정확한 값을 저장한다면, 유리수 [math(\dfrac mn)]([math(n \ne 0)])에 대해 그냥 [math(m)], [math(n)]을 저장하면 그만이다. 보통 순서쌍과 행렬과 같이 여러 개의 정수들을 저장하는 구조체를 만들고, 해당하는 값들을 저장한다. 유리수의 정의 자체가 두 정수의 비율로 표현할 수 있는 수이기 때문에, 저장도 정수 2개를 하고, 표시할 때는 그 근삿값을 표시하는 식이다. 이외 연산은 아래와 같이 구현하면 된다.
- 정수와의 연산: 정수를 분모가 1인 유리수로 가정하고 연산
- 유리수와의 연산
- 덧셈/뺄셈: m/n ± x/y = (my±nx)/ny
- 곱셈: m/n × x/y = mx/ny
- 나눗셈: m/n ÷ x/y = my/nx
- 실수와의 연산: 유리수를 m/n인 실수로 변환하고 연산
4. 컴퓨터에서 실수(實數) 표현하기
유리수는 가산집합이므로 표현 가능하지만 굳이 필요가 없어서 잘 사용하지 않는 것과 달리, 실수는 불가산집합이기 때문에 컴퓨터로 구현할 수 없다.컴퓨터 데이터의 기본은 어디까지나 전기신호인 '비트'이기 때문에 정수는 자릿수가 허용하는 범위 내의 수를 완벽히 나타낼 수 있지만, 실수는 근삿값을 저장하는게 최선이다.
종이 위에 어떤 수를 적어놓는다고 할 때, 종이가 충분히 크다면 백만자리 정수든 천만자리 정수든 한 글자도 빠짐없이 적는 게 가능하다. 반면, 종이가 아무리 커도 유한한 크기의 종이 위에 반복되지 않는 '무한한 자릿수의 숫자', 예를 들어 π(= 3.1415926535...)를 한 자리도 빠짐없이 종이 위에 적는 것은 불가능한 것과 같은 이치이다.
하지만 과학에서 소위 말하는 '데이터'에 이러한 실수가 사용되는 일은 거의 없고, 있다하더라도 애초에 공학적으로도 '근삿값'을 사용하면 전혀 문제가 없기 때문에[15] 구현의 필요성은 거의 없다.
그나마 컴퓨터에서 실수를 표현하는 방식으로 '고정 소수점 방식(Fixed Point System)'과 '부동 소수점 방식(Floating Point System)' 두 가지를 생각해볼 수 있다. 물론 '소수점'이라는 것에서 알 수 있겠지만 이 방식도 실수의 참값이 아닌 근삿값을 기록하는 것이며, 현실적으로 오차범위로 인한 문제가 없을 정도로 정밀한 자리까지 근사하여 사용하는 것이다.
4.1. 고정 소수점
Fixed point일상적으로 쓰는, 0 뒤에 소수점을 붙이는 방식을 기반으로 한다. 이를테면 8비트로 실수를 표현할 때 보통 '8비트의 앞 4비트는 정수부를, 뒤 4비트는 소수부를 나타낸다'라고 정해 놓는다. 그러면 1010.0111(2)을 '1010 0111'로, 11.011(2)은 '0011 0110'으로 나타낼 수 있다.
사실상 int와 다를 게 없으므로 구현하기 매우 편하고 연산 속도가 빠르고 시스템의 복잡도를 크게 낮출 수 있지만 이 방식으로는 표현할 수 있는 수의 범위가 너무 제한적이다. 제대로 사용하기 위해서는 표현하고자 하는 데이터와 알고리즘에 대한 수치 분석이 필요하기 때문에 고속, 고효율 연산을 요구하지 않는 어플리케이션에서 사용되는 경우는 매우 드물며, 임베디드 분야나 Digital Signal Processor, ASIC, FPGA 환경에서 주력으로 사용된다. 제어 시스템, 통신 시스템, 오디오 및 비디오 신호 처리, 금융 및 계산과학 분야 등 다양한 분야에서 사용된다.
Computer arithmetic 지식이 부족한 코더들의 경우 비트수가 동일할 때 고정소수점은 부동 소수점보다 정밀도가 무조건 떨어진다고 착각하는 경우가 많다. 실제로는 표현하고자 하는 데이터의 특성과 알고리즘에 의해 결정되며, 양자화 에러와 경계 조건을 분석하여 적절한 고정소수점 표현을 찾아내 부동 소수점을 고정 소수점으로 변환하여 정밀도를 올리는데 성공한 사례는 수두룩 빽빽하다.
4.2. 부동 소수점
Floating point고정 소수점 방식의 단점을 해결하기 위해 수를 이진수로 변환한 다음, 맨 처음 나타나는 '1'에서부터 소수점을 이동시키는 정규화 과정을 거친다. 여기서 '부동'은 움직이지 않는다는 뜻의 부동(不動)이 아니라 소수점이 '떠다닌다고 해서(float) 부동(浮動) 소수점이다. 정치권에서 쓰는 부동층과 같은 한자.[16] 영어 어구 floating point를 직역한 표현인데, 소수점 부호의 위치가 고정되어 있지 않고 떠서 움직인다는 뜻이다. 다만 '유동(流動)'이라는 더 적절한 한자어가 있었다는 점을 비춰 볼 때 아쉬운 번역. 실제로 북한에선 '류동소수점수형'이라한다. #
주어진 실수를 [math(x \times 2^y)] ([math(1 \le x < 2)], [math(y)]는 정수)꼴로 표현한 후, x, y를 저장하는 방식으로 수를 저장한다. 상용로그에서 지표와 가수를 쓰는 것과 같은 원리고, 자연과학에서도 여기 특정한 규약을 더해 측정 정밀도도 함께 표현하는 거듭제곱꼴 표현을 사용한다.
부동소수점 방식은 굉장히 넓은 범위의 수를 표현할 수 있으면서도 (상대적으로) 높은 정밀성을 보장한다. 과거에는 부동소수 연산을 위해 별도의 프로세서가 필요하던 시절도 있었지만, 인텔 80486부터 기본 구조에 연산 유닛을 포함한 CPU가 보급되기 시작되어 오늘날에는 대부분의 컴퓨터가 기본적으로 부동 소수점 방식을 사용해 실수를 표현한다.
4.2.1. IEEE Floating Point Standard(IEEE 754)
IEEE Floating Point Standard (IEEE 754)는 1985년 IEEE에서 공표한 부동 소수점 방식의 표준안으로, 오랫동안 거의 유일하게 널리 사용된 방식이고, 현재에도 가장 많은 컴퓨터 시스템에서 실수값을 표현하는데 사용하고 있다. 다만 딥 러닝 대두 이후에는 후술할 E과 M 값을 정당히 바꾼 변종들이 많아졌고 나름 여기저기서 쓰이고 있다.IEEE 754에는 표현하려는 수의 정밀도에 따라 32비트의 단정밀도(single-precision), 64비트의 배정밀도(double-precision) 등을 사용할 수 있게 하고 있다. 이 중 32비트 단정밀도는 실수값을 표현하려는 컴퓨터에서 반드시 구현되어야 하고, 나머지는 선택사항이다. IEEE 754에는 다음과 같은 내용들이 정의되어 있다.
- 산술 형식(arithmetic formats): 0을 포함한 일반적인 실수값과 양의 무한대, 음의 무한대, NaN[17] 등의 표현
- 형식의 교환(interchange formats): 부동 소수점 데이터를 교환할 때 사용할 수 있는 효율적이고 간편한 인코딩 방식
- 반올림 규칙(rounding rules): 산술적인 계산이나 변환 과정에 있어서 반올림 할 때 지켜져야 할 성질
- 연산(operations): 산술 형식으로 나타낸 데이터의 산술연산, 기타 연산
- 예외 처리(exception handling): 예외적인 조건의 표기(0으로의 나누는 작업, 오버플로 등)
4.2.1.1. 비트 배정방식
IEEE 754는 실수를 다음 식의 형태로 표현한다.(-1)s × M × 2E |
- s는 이 수의 부호(sign)를 나타낸다: 양수일 때 s = 0, 음수일 때 s = 1
- M은 유효숫자(significant)를 나타낸 값이다. Significand 또는 Mantissa라 부른다.
- E는 지수(exponent)를 뜻한다.
<rowcolor=#000>0 | 0 | 0 | … | 0 | 0 | 0 | 0 | 0 | … | 0 |
s | E | M |
실수의 표현은 필요한 정확도에 따라 따라 각
E
과 M
에 사용되는 비트의 수가 달라진다. 아래는 IEEE 754에서 정의되는 표준 포맷과, 그 밖에 같은 표현 방식을 따르되 IEEE 표준으로 정의되지 않은 부동소수점 포맷 일부의 목록이다.명칭 | 비트수 | 표현 가능한 범위 | ||||||
s | E | M | 합계 | 최소 크기 | 최대 크기 | |||
(subnormal) | (normal) | |||||||
IEEE 754 표준 | ||||||||
반정밀도 | Half-precision binary16 / FP16 | <colcolor=#000><colbgcolor=#edebe0> 1 | <colcolor=#000><colbgcolor=#ffff99> 5 | <colcolor=#000><colbgcolor=#cc99ff> 10 | 16 (2바이트) | 5.96·10-8 (2-24) | 6.10·10-5 (2-14) | 6.55·104 (1.11...(2)·215) |
단정밀도 | Single-precision binary32 / FP32 | 1 | 8 | 23 | 32 (4바이트) | 1.40·10-45 (2-149) | 1.18·10-38 (2-126) | 3.40·1038 (1.11...(2)·2255) |
배정밀도 | Double-precision binary64 / FP64 | 1 | 11 | 52 | 64 (8바이트) | 4.94·10-324 (2-1074) | 2.23·10-308 (2-1022) | 1.80·10308 (1.11...(2)·21023) |
4배정밀도 | Quadruple-precision binary128 / FP128 | 1 | 15 | 112 | 128 (16바이트) | 6.48·10-4966 (2-16494) | 3.36·10-4932 (2-16382) | 1.19·104932 (1.11...(2)·216383) |
([math(k)]-bit) | binary{[math(k)]} | 1 | [w] | [math(k-w-1)] | [math(k)] | |||
비표준 | ||||||||
FP4[19] | 1 | 2 | 1 | 4 | ||||
FP8 - E4M3 | 1 | 4 | 3 | 8 | ||||
FP8 - E5M2 | 1 | 5 | 2 | 8 | ||||
bfloat16[20] | 1 | 8 | 7 | 16 | ||||
x86 extended precision | 1 | 15 | 64[21](63) | 80 |
4.2.1.2. Normalized value의 표현
만약exp
의 값이 000...0이나 111...1이 아니라면 이는 Normalized value 형식으로 인코딩된 실수이다.인코딩 방법은 다음과 같다.
exp
= E + bias (단, bias = 2k - 1 - 1. k는exp
의 비트 수이다. 예를 들어exp
의 길이가 8비트일 때 bias = 127.)[24]frac
= (2진법으로 나타낸) M의 소수점 아래 (유효)숫자들 (이때 M = 1.xxx... 형태를 하고 있다.)[25]
- E =
exp
- bias - M = 1.(
frac
)
float f = 2003.0;
에서 f
에 어떤 2진수 값들이 들어가는지(인코딩 되는지) 알아보자.[27]우선 10진수 2003.0을 2진수로 바꿔보자.
2003.0 = 11111010011(2) = 1.1111010011(2) × 210
이므로, E = 10, M = 1.1111010011(2)이다.
이때,
exp
= E + bias이고, bias = 2k - 1 - 1 = 28 - 1 - 1 = 127이므로 exp
= 10 + 127 = 137 = 1000 1001(2)이다.또한 M = 1.1111010011(2)에서
frac
= 111 1010 0110 0000 0000 0000(2)이다.따라서 2003.0 = 0100 0100 1111 1010 0110 0000 0000 0000(2) = 0x44FA 6000으로 인코딩되어
f
에 저장된다.디코딩은 이를 역순으로 하면 된다.
위 식을 이용하면 Normalized value의 정밀도를 10진수로 환산할수 있다.
유효숫자는 M의 총 유효숫자 비트 폭을 이용하여 구할 수 있다. 단정밀도는 유효숫자가 24비트이므로, 10진수 기준으로는 약 7.225자리이다. 배정밀도는 53비트, 약 15.95자리이다.
지수는
exp
의 값이 1부터 2k-2사이이므로, bias를 뺀 E의 범위는 -2k-1+2 ~ 2k-1-1이다. 단정밀도는 k=8이므로 지수부가 2-126~2127이고, 10진수로 근사하면 10-37.93~1038.23이다. 배정밀도는 k=11이므로 2-1022~21023(≒10-307.65~10307.95)이다.4.2.1.3. Denormalized value의 표현
만약exp
= 000...0이라면 이는 Denormalized value 형식으로 인코딩된 실수이다.인코딩 방법은 다음과 같다.
exp
= 000...0 (고정)frac
= (2진법으로 나타낸) M의 소수점 아래 (유효)숫자들 (이때 M = 0.xxx... 형태를 하고 있다.)[28]
- E = 1 - bias
- M = 0.(
frac
)
Denormalized value가 표현하는 범위는 0과 그 주위의 (절대값이) 매우 작은 수들이다.
즉,
float
로 0은 0000 0000 0000 0000 0000 0000 0000 0000(2) = 0x0000 0000 또는 1000 0000 0000 0000 0000 0000 0000 0000(2) = 0x8000 0000으로 표현할 수 있다.[29]단정밀도(binary32)에서 Denormalized value가 표현할 수 있는
float
형의 0이 아닌 가장 작은 양수는 0.000 0000 0000 0000 0000 0001(2) × 2-126(=2-149)이다.[30] 이보다 작은 양수는 IEEE 754에서는 표현할 수 없어서 모두 0000 0000 0000 0000 0000 0000 0000 0000(2) = 0x0000 0000으로 인코딩해 버린다.[31] 이는 IEEE 754의 실수 표현에는 '하한'이 존재한다는 것을 보여준다.4.2.1.4. Special value의 표현
만약exp
= 111...1이라면 이는 특수한 수들을 표현하기 위해 예약되어 있는 수들이다.exp
= 111...1이고frac
= 000...0인 수는 무한(±∞)을 표현하기 위한 수이다.float
+∞ = 0111 1111 1000 0000 0000 0000 0000 0000(2) = 0x7F80 0000float
-∞ = 1111 1111 1000 0000 0000 0000 0000 0000(2) = 0xFF80 0000exp
= 111...1이고frac
≠ 000...0인 수는 NaN(Not-A-Number)을 표현하기 위한 수이다. [math(sqrt{-1})] 등의 수나, ∞ - ∞, ∞ × 0, 0 ÷ 0, [math(\ln \left( -1 \right) (= i\pi))] 등의 (실수로) 정의되지 않는 표현식의 결과를 나타내는 데 사용된다.
각 표현 체계로 나타낼 수 있는 수의 범위는 다음과 같다.
← 작다 | 크다 → | 기타 | |||||||
-∞ | 일반적인 음의 실수 | 0 근처의 절댓값이 매우 작은 음수 | -0 | +0 | 0 근처의 절댓값이 매우 작은 양수 | 일반적인 양의 실수 | +∞ | NaN | |
Special | Normalized | Denormalized | Normalized | Special |
5. 컴퓨터에서 복소수 표현하기
복소수를 구현하는 표준 방법이 정의되지는 않았다. 구현되는 프로그램의 대다수는 복소수를 구현할 필요가 없는 프로그램이기 때문.다만 유리수처럼 구현하려면 구현할 수는 있다. 복소수의 정의가 실수 a + 허수 bi로 표현되는 수이기 때문에, 두 수를 각각 저장할 수 있는 구조체를 만들면 된다.
- 출력: [math(a+bi)] 형태로 출력
- 실수와의 연산: 실수를 [math(b)]가 [math(0)]인 복소수로 취급하고 연산
- 복소수와의 덧셈: [math((a+bi)+(c+di)=(a+c)+(b+d)i)]
- 복소수와의 곱셈: [math((a+bi)*(c+di)=(ac-bd)+(ad+bc)i)]
울프럼 알파나 매스매티카와 같은 프로그램에서는 위와 같이 구현한다. 프로그래밍 언어의 일종인 Python에서도 complex라는 자료형이 있어서 위와 같은 방식으로 구현된다. C#에서는 System.Numerics.Complex 구조체로 표현한다. 수치 계산용 언어인 Julia의 경우 언어 문법(리터럴) 차원에서 복소수를 지원하기도 한다. 포트란의 경우 기본적으로 a+bi를 (a,b)로 표현해 계산한다.
6. 관련 문서
[1] 자주 쓰이지 않는 128비트 이상의 정수형은 제외한다.[2] AMD64 기준[c] C표준에는 최소 비트수만 정의되어 있으므로 컴파일하는 환경에 따라
int
는 최소 16비트, long
은 최소 32비트가 될 수 있지만, 본 표에서는 가장 널리 쓰이는 64비트 컴퓨터에서 흔히 받아들여지는 기준으로 표기한다. 다만 long
과 long long
은 흔히 64비트 환경 기준에선 둘다 64비트이기 때문에 병기한다. 때문에 또한 키워드를 생략 가능한 경우는 가장 적은 키워드를 사용하는 표기를 기준으로 한다. 예: signed short int
-> short
[c] [c] [6] 주소가 배정되어 있어야 접근이 가능하기에 중요하다.[7] 컴퓨터활용능력 필기 시험에서는 이 용어를 쓴다.[8] 예를 들어 000011(2) + 111100(2) = 111111(2) = 0. 1의 보수 방법에서는 모든 비트가 1인 수도 0으로 취급하는데 왜냐하면 0 = 000000(2)을 1의 보수화하면 -0 = 111111(2)이 되기때문이다. 즉, 1의 보수 방법에서는 0이 0 = 000000(2) 과 -0 = 111111(2) 두가지로 존재한다.[9] 왼쪽 가장 마지막 자리(비트)에서 받아올림(Carry)이 발생하는 상황. 이 경우 1의 보수 방법에서는 마지막 자리의 받아올림을 무시한 계산결과에 1을 더해야한다.[10] 2의 보수 방법으로 이진수를 구하는 다른 방법도 있다. 오른쪽에서부터 1이 나올 때까지 그대로 둔 후, 1이 나오게 되면 그 다음부터 모든 자리를 반전시키는 방법이다. 예를 들어, -12를 이진수로 표현한다고 했을 때, 12의 이진수 표현인 001100(2)에서 맨 오른쪽부터 스캔하게 되면 밑줄친 1이 최초로 등장하는 1이고, 여기까지는 그대로 두고 그 다음 자리는 모두 반전하게 되면 110100(2)가 되는데, 이게 바로 -12가 된다. 다른 예로 5 = 000101(2)라면, 밑줄친 1이 오른쪽에서 최초로 등장하는 1이므로, 그 1의 바로 왼쪽을 모두 반전한 111011(2)가 바로 -5가 된다.[11] 1 = ...001(2)를 2의 보수화하면 -1 = ...111(2)가 된다.[12] 예를 들면 부호 숫자(Signed Digit) 방법으로 수를 표현하면 덧셈 연산에서 받아올림이 발생하지 않는다는 장점을 이용해서 덧셈 연산을 많이 해야 하는 회로에서 부호 숫자를 사용한 고속 연산이 가능하다.[13] 부호 절대값, 1의 보수 방법보다 표현할 수 있는 수가 하나 더 많은데, 2의 보수에서는 +0과 -0의 개념이 없기 때문이다.[14] 당연히 16비트, 64비트 정수에서는 성립하지 않는다(15, 63이 합성수이므로).[15] 대표적인 무리수인 원주율도 공학에선 5~6자리 정도로도 별 문제 없고, 나노미터 단위의 초정밀 공정에서 사용한다고 해도 10자리 정도면 충분하다. 그 이상은 말그대로 이론의 영역에 불과하다. 관측 가능한 우주 범위에서 플랑크 길이 규모로 정밀한 연산을 하려고 해도, 10진법 기준으로 62자리, 2진법으로는 205자리(26바이트)면 충분하다.[16] C언어 등을 처음 배울 때 부동소수점 개념에서 부동의 의미를 이렇게 설명하면 된다.[17] Not a Number의 약자로, 0 ÷ 0 같은 연산의 결과값처럼 수학적으로 계산 불가능한 값을 나타낼 때 쓰인다.[w] [math(\lfloor 4 \log_2 k + \frac{1}{2} \rfloor - 13)][19] IEEE 754 표준의 모든 요구사항을 만족할 수 있는 가장 작은 포맷. 3비트 이하부터는 음수를 포기하거나(s0E2M1), Normalized value를 포기하거나(s1E1M1), Inf와 NaN의 구분을 포기해야(s1E2M0) 한다.[20] Google Brain (현재 구글 딥마인드) 팀이 개발하여 Brain floating point을 줄여 bfloat라고 한다.[21] bit 63이 significand의 정수 부분을 표현하는 데 사용되며, 80387 이후의 x87 구현은 exponent 값에 따라 bit 63의 값이 하나로 정해지므로 실질적으로는 63비트이다.[22] 표현하는 범위가 다르다[23] 10진수를 2진수로 바꾸는 과정을 의미한다.[24] 이렇게 함으로써 exp
는 언제나 양수가 된다.[25] 이렇게 첫 자리 1을 생략함으로써 우리는 유효숫자를 한 자리 더 표현할 수 있게 되었다![26] 2진수를 10진수로 바꾸는 과정을 의미한다.[27] float
형은 8비트로 exp
를, 23비트로 frac
을 나타낸다.(단정밀도)[28] Normalized value를 표현할 때와 다르게 정수부가 1이 아닌 0임에 주의하라![29] 엄밀히 표현하면 앞의 수 0x0000 0000은 +0, 뒤의 수 0x8000 0000는 -0을 의미한다.[30] 0000 0000 0000 0000 0000 0000 0000 0001(2) = 0x0000 0001으로 인코딩된다.[31] 이를 gradual overflow(점진적 오버플로)라 한다.