최근 수정 시각 : 2024-12-30 09:55:53

컴퓨터에서의 수 표현

파일:상위 문서 아이콘.svg   상위 문서: 컴퓨터/표현
1. 개요2. 컴퓨터에서 정수 표현하기
2.1. 메모리에 저장하는 방법
2.1.1. 빅엔디언, 리틀엔디언
2.2. 표현법
2.2.1. (signed) 음수를 표현
2.2.1.1. 양수를 표현할 때2.2.1.2. 음수를 표현할 때
2.2.1.2.1. 부호화 절대치2.2.1.2.2. 1의 보수2.2.1.2.3. 2의 보수
2.2.2. (unsigned) 음수를 표현 안 함2.2.3. (casting) 형 변환
2.3. 정수의 연산
2.3.1. unsigned의 덧셈2.3.2. signed의 덧셈2.3.3. signed의 음수화(negation)2.3.4. unsigned, signed의 곱셈
3. 컴퓨터에서 유리수 표현하기4. 컴퓨터에서 실수(實數) 표현하기
4.1. 고정 소수점4.2. 부동 소수점
4.2.1. IEEE Floating Point Standard(IEEE 754)
4.2.1.1. 비트 배정 방식4.2.1.2. normalized value의 표현4.2.1.3. denormalized value의 표현4.2.1.4. special value의 표현
5. 컴퓨터에서 복소수 표현하기6. 관련 문서

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 Frameworklong형은 64비트이다. 정수형의 크기가 중요한 프로그램을 개발한다면 Cint32_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으로 분류할 수 있다.

파일:상세 내용 아이콘.svg   자세한 내용은 엔디언 문서
번 문단을
부분을
참고하십시오.

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) = 1073741824
0000 0100 1001 1000 0000 0000 0011 1111(2) (0x0498003F) = 77070399
0000 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 complement

1의 보수 방법에 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의 보수 방법을 이용한 뺄셈 연산을 주로 채용하고 있다.

파일:5-19-4.png

컴퓨터는 보통 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)
이 나온다. 여기서 밑줄 친 1은 받아올림이 일어난 부분을 의미한다. 이때 컴퓨터는 이 1을 무시하여 계산 결과를 0101(2)로 인식한다. 따라서 9 + 12 = 0101(2) = 5로 계산된다.

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))
곱셈에 관해서는 조금 흥미로운 이야깃거리가 있다. 만약 곱하는 두 수 x, y 중 하나가 상수라면 대부분의 컴파일러에서는 곱셈 연산이 아닌 비트 이동(bit shift), 덧셈 등의 연산을 수행하는 것으로 최적화를 지원한다(최적화 옵션에 따라 이렇게 최적화하지 않을 수도 있다). 이는 곱셈이 덧셈이나 비트 이동에 비해서 더 많은 자원을 사용하기 때문이다.
예를 들어 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으로의 나누는 작업, 오버플로 등)
이 외에도 IEEE 754에는 더 복잡한 예외 처리, 추가적인 작업(삼각 함수 등), 수식의 계산 등에 대한 정의가 포함되어 있다. IEEE 754에는 10진 부동 소수점도 존재하는데 .NET 등지에서 사용한다.
4.2.1.1. 비트 배정 방식
IEEE 754는 실수를 다음 식의 형태로 표현한다.
(-1)s × M × 2E
  • s는 이 수의 부호(sign)를 나타낸다: 양수일 때 s = 0, 음수일 때 s = 1
  • M은 유효 숫자(significant)를 나타낸 값이다. Significand 또는 Mantissa라 부른다.
  • E는 지수(exponent)를 뜻한다.
이를 다음과 같이 Encoding한다.
<rowcolor=#000>0 0 0 0 0 0 0 0 0
s E M

실수의 표현은 필요한 정확도에 따라 따라 각 EM에 사용되는 비트의 수가 달라진다. 아래는 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
IEEE 754에서는 normalized value, denormalized value, special value 세 가지 값들[22]에 대해 다른 인코딩[23] 방법을 적용한다.
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]
디코딩[26] 방법은 다음과 같다.
  • 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 0000
    • float -∞ = 1111 1111 1000 0000 0000 0000 0000 0000(2) = 0xFF80 0000
  • exp = 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비트 컴퓨터에서 흔히 받아들여지는 기준으로 표기한다. 다만 longlong 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(점진적 오버플로)라 한다.

분류