컴퓨터에서의 수 표현

문서:고정소수점에서 넘어옴

역사 raw
대문 랜덤 문서 최근 토론
분류
1. 개요2. 컴퓨터에서 정수 표현하기
2.1. 메모리에 저장하는 방법
2.1.1. Big Endian[anchor(BE)]2.1.2. Little Endian[anchor(LE)]2.1.3. Bi Endian
2.2. 표현법
2.2.1. [[정수|음수를 표현하는 경우 (signed int)]]
2.2.1.1. 양수를 표현할 때2.2.1.2. 음수를 표현할 때
2.2.2. [[범자연수|음수를 표현하지 않는 경우(unsigned int)]]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. 컴퓨터에서 실수 표현하기[anchor(실수)]
4.1. IEEE Floating Point Standard (IEEE 754)4.2. 실수의 표현
4.2.1. Normalized value의 표현4.2.2. Denormalized value의 표현4.2.3. Special value의 표현
5. 컴퓨터에서 [[복소수]] 표현하기6. 관련 문서

1. 개요

컴퓨터10진수가 아닌 2진수로 수를 표현한다. 이 문서는 일반적인 32bit 컴퓨터[1]정수실수를 어떻게 표현하는지를 정리한 문서이다.

2. 컴퓨터에서 정수 표현하기

일반적으로 컴퓨터에서 사용되는 정수형의 종류는 다음과 같다.

크기
char
8비트
short
16비트
int
32비트
long
32비트
각 정수는 음수를 표현할 수 없고 양수 크기가 두 배로 지원되는 unsigned 형을 가진다. 위 표의 크기는 32비트 윈도우를 기준으로 한 것이다. 운영 체제, CPU 아키텍처, 프로그래밍 언어에 따라 크기나 형의 이름이 다를 수 있다. 예를 들면 같은 윈도 시스템에서도 .NET Frameworklong형은 64비트이다. 정수형의 크기가 중요한 프로그램을 개발한다면 Cint32_t와 같이 각 언어나 프레임워크에서 제공되는 크기가 명시적으로 표현된 자료형을 사용하도록 하자. 단, 이 경우 stdint.h 헤더를 선언해야 한다.

2.1. 메모리에 저장하는 방법

메모리에서는 정수 데이터를 저장하기 위해 4칸을 쓰게 된다. 바이트는 컴퓨터가 정보를 저장하는 가장 작은 단위이자 메모리 상에서 주소가 배정될 수 있는(addressable) 가장 작은 단위이다. 메모리는 바이트 단위로 주소가 배정되어 있고(주소가 배정되어 있어야 접근이 가능하기에 중요하다.), 정수는 4바이트이므로 4칸이 필요하다. 당연하지만 64비트 수 체계는 이의 2배인 8칸을 차지한다. 4칸의 메모리에 정수를 어떤 순서로 저장하냐에 따라 Big Endian, Little Endian, Bi Endian으로 분류할 수 있다.

뜬금없이 인디언도 아니고 엔디언이라는 용어가 나오는데, 이건 조너선 스위프트의 작품인 《걸리버 여행기》에서 유래한 단어다. 작중 릴리퍼트라는 난쟁이들이 사는 나라에서 달걀을 먹을 때 뭉툭한 끝을 깨먹은 사람들과 뾰족한 끝을 깨먹는 사람들이 자기들이 옳다며 논쟁을 벌이는데, 여기서 뭉툭한 끝을 깨먹는 사람들을 큰 끝(big end)을 깨먹는다고 ian을 붙여 big endian이라고 부르고, 반대의 경우를 작은 끝(little end)을 깨먹는다고 little endian이라고 부른다. 큰 끝을 깨든 작은 끝을 깨든 잘 먹기만 하면 상관없는 것을 쓸데없이 다투는 게, 마치 왼쪽부터 저장하던 오른쪽부터 저장하건 저장만 잘 되면 상관없는 메모리 저장 순서를 가지고 다투는 것과 비슷해 보여서 이런 이름이 붙었다고 한다. 물론 이건 호환이 되어야 하니까, 그리고 가끔 관련 자료를 해석할 때 필요하니까 실제로는 중요한 문제다.

이 문단에서 사용할 중요한 용어는 다음과 같다.
  • MSB (Most Significant Byte): '최상위 바이트'라는 뜻으로, 가장 큰 자릿수를 담당하는 바이트를 지칭하는 말이다. 예를 들어, 0x12345678에서는 12를 담고 있는 바이트가 MSB이다.
  • LSB (Least Significant Byte): '최하위 바이트'라는 뜻으로, 가장 작은 자릿수를 담당하는 바이트를 지칭하는 말이다. 예를 들어, 0x12345678에서는 78을 담고 있는 바이트가 LSB이다.
여기에서 significant는 '중요하다'는 뜻이 아니라 'most significant' 자체가 숙어로, '최상위의', '맨 앞자리의'라는 뜻이다. 예를 들어 '12345'에서 most significant한 자리는 맨 앞의 '1'이다.

2.1.1. Big Endian

MSB가 가장 앞쪽에 오는 저장 방법. 예를 들어, 메모리에 0x12345678을 저장한다고 하면,
12
34
56
78
←작은 주소
큰 주소 →
이런 식으로 저장하는 것이다. 이때 표에서 한 칸은 1바이트를 의미한다. 보통 IBM이나 썬 마이크로시스템즈의 컴퓨터들이 이 방식을 채택한다. JavaJVM은 호스트 운영체제나 하드웨어와 상관 없이 항상 Big Endian을 사용한다. 쉽게 말하면 주소를 오름차순으로 기록하는 방식이다.

사람이 보기에 직관적이라는 장점이 있다.

네트워크로 데이터를 주고받을 때에는 항상 Big Endian 형태로 다루도록 약속되어 있다.

2.1.2. Little Endian

LSB가 가장 앞쪽에 오는 저장 방법. 예를 들어, 메모리에 0x12345678을 저장한다고 하면,
78
56
34
12
←작은 주소
큰 주소 →
이런 식으로 저장하는 것이다. 보통 인텔사의 칩셋과 호환이 되는 컴퓨터들이 이 방식을 채택한다.

네트워크로 데이터를 전송할 때에는 모두 빅 엔디안 형식으로 변환하고, 네트워크에서 수신한 데이터를 활용할 때에는 다시 리틀 엔디안으로 변환하는 번거로움이 있다. 반면 데이터 캐스팅이 빠르다는 장점이 있다. 예를 들어 빅 엔디안에서는 32비트 변수를 16비트로 캐스팅하면 뒤의 두 바이트를 따로 복사해 와야 하지만 리틀 엔디안에서는 그냥 앞의 두 바이트만 읽어주면 된다.

쉽게 말하면 주소를 내림차순으로 기록하는 방식이다.

2.1.3. Bi Endian

컴퓨터 시스템이, 또는 경우에 따라 사용자가 직접 데이터 저장 방식을 Big Endian과 Little Endian 중에서 고를 수 있는 형태. PowerPC에서는 부팅 시의 설정으로 빅 엔디안과 리틀 엔디안을 선택할 수 있다.

자신의 컴퓨터가 어느 저장 방법을 따르는지를 알고 싶으면 간단한 C 코드를 통해 이를 체크할 수 있다.[2]
/*Endian_chk.c
*
*이진수 0000 0000 / 0000 0000 / 0000 0000 / 0000 0001(2)을 i에 저장한 후, 정수형 변수 i를 문자형 변수로 캐스팅한다.
*그렇게 하면 4바이트의 i는 1비트의 길이 4인 배열이 된다.
*이제 이 배열의 시작 주소를 읽는다.
*이 값이 1이면 가장 마지막 자리가 가장 작은 주소에 저장되어 있다는 것(0000 0001(2))이므로 Little Endian이다.
*이 값이 0이면 가장 큰 자리가 가장 작은 주소에 저장되어 있다는 것(0100 0000(2))이므로 Big Endian이다.
*/

#include <stdio.h>

int main()
{
    int i = 0x00000001;
    if( ((char *)&i)[0] )
        printf( "Little Endian\n" );
    else
        printf( "Big Endian\n" );
} 


2.2. 표현법

2.2.1. 음수를 표현하는 경우 (signed int)

총 32개의 비트 중 첫 번째 비트를 부호 표현을 위해 따로 배정한다. 이를 부호 비트(signed bit)라 부른다. 부호 비트가 0이면 양수를, 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가지를 꼽자면 '부호화 절대치[3] 방법(Sign Magnitude)', '1의 보수 방법(1's Complement)', '2의 보수 방법(2's Complement)'가 있다. 보수 방법은 컴퓨터에서 쓰이는 양수가 전체 자연수가 아니라 0부터 상한까지 자른 부분집합이라는 성질을 잘 이용한 것이다.

  • 부호화 절대치 방법(Signed Magnitude): 부호 비트를 제외한 수를 양수로 읽고, 마이너스를 붙이는 방법. 즉 이진수 000011(2) = +3으로, 100011(2) = -3으로 인식하는 것이다. 이는 인간 입장에서 표기하기 직관적이고 곱셈이나 나눗셈을 할 때 매우 유리하지만, 음수의 덧셈이 양수의 뺄셈과 전혀 딴판이라는 어이없는 결과가 나오므로 이를 해결하기 위해 피연산자의 절댓값을 서로 비교하는 등 추가적인 연산을 필요로 한다는 단점이 있다.
    • 예를 들어 000011(2)과 100011(2)을 이진수 계산으로 더하면 000011(2) + 100011(2) = 100110(2) = -6이 되는데, 이는 결과값으로 나와야 할 (+3) + (-3) = 0과는 다른 값이다.
  • 1의 보수 방법(1's Complement): 양수값의 비트들을 반전시켜서 음수를 표현하는 방법.[10] 즉 이진수 000011(2) = +3의 비트를 모두 반전시켜 111100(2)을 만들어 -3을 표현하는 방법이다. 1의 보수 방법에서는 2진수 연산값이 실제 값과 같다.[11] 하지만 0을 나타내는 값이 000...00(2)(모든 비트가 0인 수)[12]과 111...11(2)(모든 비트가 1인 수)[13] 두 가지가 나오고, 덧셈 연산을 할 때 end around carry가 발생해서 1을 더해줘야 할 때가 있다는 단점이 있다..
  • 2의 보수 방법(2's Complement): 1의 보수 방법에 1을 더하는 방법.[14][15] 즉 이진수 000011(2) = +3의 비트를 모두 반전시켜 111100(2)을 만들고, 여기에 1을 더해 111101(2)로 -3을 표현하는 방법이다. 2의 보수 방법에서는 1의 보수 방법에서와 다르게 111...11(2)이 의미하는 값이 -1을 의미하므로 000...00(2) = 0과 구분된다. 다만 음의 부호를 붙일 때 1을 더해주는 연산을 해야 한다는 단점이 있다. 하지만 그것이 2의 보수 방법에서 감수해야 하는 유일한 불편함이고, 양수를 빼는(음수를 더하는) 연산을 할 때 추가 작업을 해줄 필요가 없고 다른 방법들과 달리 +0과 -0을 구별 안 해도 되므로 장점이 훨씬 더 많다. 따라서 컴퓨터에서는 2의 보수 방법을 이용한 뺄셈 연산을 채용하고 있다.

파일:5-19-4.png
<파이썬 알고리즘 인터뷰> p.548, 책만, 2020

컴퓨터는 보통 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) 등의 방법들은 특정 회로에서 유용함이 입증되어 하드웨어 레벨에서 내부적으로 사용하는 경우가 있다.[16]

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 int)

컴퓨터에서는 정수를 표현할 때 경우에 따라서는 음수를 표현하지 않아도 될 때가 있다. 이 때는 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)[18]

unsigned int에서는 4비트, 8비트, 32비트, 128비트 한정으로 [math((a+b)^n = a^n+b^n)]이 성립한다. 이는 메르센 소수에 속하는 조건이기 때문에 성립하는 것.[19]

2.2.3. 형 변환(casting)

형 변환을 할 때의 규칙은 다음과 같다.
  • 각 비트의 숫자들은 그대로 유지한다.
  • 각 비트를 해석(interpret)하는 방법을 다르게 한다.

예를 들어, 6비트의 signed 변수로 표현된 -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비트짜리 unsigned int의 경우 232=4294967295를 넘길 경우 발생하지만 64비트 정도면 일부러 오버플로우를 내려 하지 않는 이상 거의 안 난다고 보는 것이 맞을 것이다.

예를 들어, 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. 컴퓨터에서 유리수 표현하기

정의하려고 하면 할 수는 있지만, 사실상 상위 집합인 실수가 있기에 실제로 구현해야 할 경우는 거의 없다. 실수를 쓰는 경우는 대부분 특정한 공식의 결과값을 표시하는 것이 아닌 생 데이터를 그대로 표현해야 하는 경우가 많아 굳이 유리수를 써야 할 이유가 없기 때문. 그래도 구현하려면 할 수는 있는데, 숫자 두 개를 저장하는 구조체와 해당하는 연산을 만들면 된다. 유리수의 정의 자체가 두 정수 m, n으로 m/n을 표현할 수 있는 수이기 때문에, 저장은 m, n을, 표시할 때에는 m/n의 근삿값을 표시하면 된다. 이외 연산은 아래와 같이 구현하면 된다.
  • 정수와의 연산: 정수를 분모가 1인 유리수로 가정하고 연산
  • 유리수와의 연산
    • 덧셈/뺄셈: m/n ± x/y = (my±nx)/ny
    • 곱셈: m/n × x/y = mx/ny
    • 나눗셈: m/n ÷ x/y = my/nx
  • 실수와의 연산: 유리수를 m/n인 실수로 변환하고 연산

4. 컴퓨터에서 실수 표현하기

한 가지 명확히 해두어야 할 것은 정수와는 다르게 실수는 컴퓨터 상에서 완벽하게 표현할 수 없다는 것이다. 정수는 자릿수가 허용하는 범위에서 모든 수를 완벽히 나타낼 수 있지만, 실수는 그 특성상 수를 완벽하게 나타낼 수 없고 (대부분의 경우) 오차가 생기게 된다. 종이 위에 숫자를 적어놓는다고 할 때, 종이가 충분히 크다면 백만자리 정수든 천만자리 정수든 한 글자도 빠짐없이 적는 게 가능하지만, 아무리 종이가 크다고 하더라도 유한한 크기의 종이 위에 1/3(0.3333333333333....)을 한 글자도 빠짐없이 종이 위에 적는건 불가능한 것과 같은 이치이다.

컴퓨터에서 실수를 표현하는 방식으로 '고정 소수점 방식(Fixed Point System)'과 '부동 소수점 방식(Floating Point System)' 두 가지를 생각해볼 수 있다.
  • 고정 소수점 방식: 특정 위치에 소수점을 고정해 놓고 그 앞자리에는 실수의 정수부를, 뒷자리에는 실수의 소수부를 나타내는 방식이다. 예를 들어, 8비트로 실수를 나타내는 컴퓨터가 있다고 할 때 우리는 사전에 '8비트의 앞 4비트는 정수부를, 뒤 4비트는 소수부를 나타낸다'라는 약속을 해 놓는다. 그러면 1010.0111(2)을 '1010 0111'로, 11.011(2)은 '0011 0110'으로 나타낼 수 있다. 사실상 int와 다를 게 없으므로 구현하기 매우 편하고 연산 속도가 빠르고 시스템의 복잡도를 크게 낮출 수 있지만 이 방식으로는 표현할 수 있는 수의 범위가 아주 작다. 정밀도를 높이면 표현할 수 있는 수의 크기가 작아지고, 표현할 수 있는 수의 크기를 늘리면 정밀도가 낮아지며, 둘 모두를 높이면 저장해야 하는 비트 수가 비약적으로 늘어난다. 따라서 표현하고자 하는 수의 범위가 작을 때 높은 정밀도가 필요하거나, 단순해야 하는 시스템을 구성할 때 사용된다. 예를 들어 임베디드 시스템에서 인공신경망 연산을 수행하는 경우가 있다. 임베디드 시스템 특성상 연산 능력이 부족하기 때문에 단순해야 하며, 사실 인공신경망 연산에 쓰이는 웨이트 값들이란 게 죄다 학습을 통해 얻는 근사값이기 때문에 부동소수점까지 쓸 정도로 정밀할 필요는 없다. 다른 예로 FPGA에서는 부동 소수점을 사용한 병렬 시스템을 구성하려면 연산장치의 리소스가 너무 커지고 전력 소모가 증가하기 때문에 가능하면 고정 소수점 방식으로 시스템을 설계하려는 경향이 있다.
  • 부동 소수점 방식: 고정 소수점 방식과는 다르게 소수점이 말 그대로 '떠다니면서' 실수를 표현하는 방식. 한자로 부동(不動)이 아니라 '부력'의 '부'자를 따서 만든 부동()이다. 영어 단어 floating 을 직역한 표현인데, 소수점 부호의 위치가 고정되어 있지 않고 떠서 움직인다는 뜻이다. 다만 '유동(流動)'이라는 더 적절한 한자어가 있었다는 점을 비춰 볼 때 아쉬운 번역.[21] 주어진 실수를 [math(x times 2^y)] ([math(1 le x < 2)], [math(y)]는 정수)꼴로 표현한 후, x, y를 저장하는 방식으로 수를 저장한다. 상용로그에서 지표와 가수를 쓰는 것과 같은 원리고, 자연과학에서도 여기 특정한 규약을 더해 측정 정밀도도 함께 표현하는 거듭제곱꼴 표현을 사용한다. 부동소수점 방식은 굉장히 넓은 범위의 숫자를 표현할 수 있으면서도 (상대적으로) 높은 정밀성을 보장한다. 물론 위에서 말했듯이 완벽하게 정밀할 수는 없다. 고정 소수점 방식에 비하면 덧셈이나 곱셈 계산이 복잡해지고 그만큼 연산시간도 늘어나지만, 대신 표현할 수 있는 범위가 매우 넓다. 대부분의 컴퓨터는 부동 소수점 방식을 사용해 실수를 표현한다. 물론 확실한 정밀도가 필요하다면 프로그래머 재량에 따라 소프트웨어적으로 고정 정밀도 소수를 이용할 수도 있다. 물론 이런 경우에는 하드웨어 지원이 되는 하드웨어가 극히 적다. 처리 속도를 포기하고 String 기반 고정 소수점 라이브러리를 짜든가...

4.1. IEEE Floating Point Standard (IEEE 754)

IEEE Floating Point Standard (IEEE 754)는 1985년 IEEE에서 공표한 부동 소숫점 방식의 표준안으로, 현재 가장 많은 컴퓨터 시스템에서 실수값을 표현하는데 사용하고 있다. IEEE 754에는 표현하려는 수의 정밀도에 따라 32비트의 단정도(single-precision), 64비트의 배정도(double-precision) 등을 사용할 수 있게 하고 있다. 이 중 32비트 단정도는 실수값을 표현하려는 컴퓨터에서 반드시 구현되어야 하고, 나머지는 선택사항이다. IEEE 754에는 다음과 같은 내용들이 정의되어 있다.
  • 산술 형식(arithmetic formats): 0을 포함한 일반적인 실수값과 양의 무한대, 음의 무한대, NaN[23] 등의 표현
  • 형식의 교환(interchange formats): 부동 소수점 데이터를 교환할 때 사용할 수 있는 효율적이고 간편한 인코딩 방식
  • 반올림 규칙(rounding rules): 산술적인 계산이나 변환 과정에 있어서 반올림 할 때 지켜져야 할 성질
  • 연산(operations): 산술 형식으로 나타낸 데이터의 산술연산, 기타 연산
  • 예외 처리(exception handling): 예외적인 조건의 표기(0으로의 나누는 작업, 오버플로우 등)
이외에도 IEEE 754에는 더 복잡한 예외 처리, 추가적인 작업(삼각함수 등), 수식의 계산 등에 대한 정의가 포함되어 있다.

4.2. 실수의 표현

IEEE 754는 실수를 다음 식의 형태로 표현한다.
(-1)s × M × 2E
  • s는 이 수의 부호(sign)를 나타낸다: 양수일 때 s = 0, 음수일 때 s = 1
  • M은 유효숫자(significant)를 나타낸 값이다. significand라 부른다.
  • E는 지수(exponent)를 뜻한다.
이를 다음과 같이 Encoding한다.
0
0
0
0
0
0
0
0
0
s
exp
frac
  • MSB s는 부호 비트이다.
  • exp는 E를 나타낸다.
  • frac은 M을 나타낸다.
단정도(single-precision)의 경우 8비트로 exp를, 23비트로 frac을 나타낸다.[24] 배정도(double-precision)의 경우 11비트로 exp를, 52비트로 frac을 나타낸다.[25]

IEEE 754에서는 Normalized value, Denormalized value, Special value 세 가지 값들[26]에 대해 다른 인코딩[27] 방법을 적용한다.

4.2.1. Normalized value의 표현

만약 exp의 값이 000...0이나 111...1이 아니라면 이는 Normalized value 형식으로 인코딩된 실수이다.
인코딩 방법은 다음과 같다.
  • exp = E + bias (단, bias = 2k - 1 - 1. k는 exp의 비트 수이다. 예를 들어 exp의 길이가 8비트일 때 bias = 127.)[30]
  • frac = (2진법으로 나타낸) M의 소숫점 아래 (유효)숫자들 (이때 M = 1.xxx... 형태를 하고 있다.)[31]
디코딩[32] 방법은 다음과 같다.
  • E = exp - bias
  • M = 1.(frac)
예로서 float f = 2003.0;에서 f에 어떤 2진수 값들이 들어가는지(인코딩 되는지) 알아보자.[33]

우선 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에 저장된다.

디코딩은 이를 역순으로 하면 된다. 예제쓰기 귀찮다.

2014년 국회직 전산 컴퓨터일반에 출제되었다. 국가직/지방직9급에 나올 난이도가 아닌 듯하다

4.2.2. Denormalized value의 표현

만약 exp = 000...0이라면 이는 Denormalized value 형식으로 인코딩된 실수이다.
인코딩 방법은 다음과 같다.
  • exp = 000...0 (고정)
  • frac = (2진법으로 나타낸) M의 소숫점 아래 (유효)숫자들 (이때 M = 0.xxx... 형태를 하고 있다.)[35]
디코딩 방법은 다음과 같다.
  • 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으로 표현할 수 있다.[36]

Denormalized value가 표현할 수 있는 float형의 0이 아닌 가장 작은 양수는 0.000 0000 0000 0000 0000 0001(2) × 2-126이다.[37] 이보다 작은 양수는 IEEE 754에서는 표현할 수 없어서 모두 0000 0000 0000 0000 0000 0000 0000 0000(2) = 0x0000 0000으로 인코딩해 버린다.[38] 물론 더 많은 비트를 사용하여 실수를 표현하면 더 작은 수까지도 표현이 가능하다. 이는 IEEE 754의 실수 표현에는 '하한'이 존재한다는 것을 보여준다.

4.2.3. 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) (= ipi))] 등등의 (실수로) 정의되지 않는 표현식의 결과를 나타내는 데 사용된다.

각 표현 체계로 나타낼 수 있는 수의 범위는 다음과 같다.
← 작다
크다 →
기타
-∞
일반적인 음의 실수
0 근처의 절댓값이 매우 작은 음수
-0
+0
0 근처의 절댓값이 매우 작은 양수
일반적인 양의 실수
+∞
NaN
Special
Normalized
Denormalized
Normalized
Special

5. 컴퓨터에서 복소수 표현하기

복소수를 구현하는 표준 방법이 정의되지는 않았다. 구현되는 프로그램의 대다수는 복소수를 구현할 필요가 없는 프로그램이기 때문.

다만 유리수처럼 구현하려면 구현할 수는 있다. 복소수의 정의가 실수 a + 복소수 bi로 표현되는 수이기 때문에, 두 수를 각각 저장할 수 있는 구조체를 만들면 된다.

  • 출력: a+bi 형태로 출력
  • 실수와의 연산: 실수를 b가 0인 복소수로 취급하고 연산
  • 복소수와의 덧셈: (a+bi) + (c+di) = (a+c) + (b+d)i
  • 복소수와의 곱셈: (a+bi) * (c+di) = (ac-bd) + (ad+bc)i

울프램알파매스매티카와 같은 프로그램에서는 위와 같이 구현한다.

6. 관련 문서

[1] 2010년대부터는 x86-64 아키텍처가 궤도에 본격적으로 오르기 시작하면서 64비트 수 표현을 기본으로 지원하게 되었으나, 아직까지는 멀었다. 대부분의 64bit 시스템은 호환성을 위해 32bit 시스템도 지원한다.[2] 출처: http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/Network_Programing/Documents/endian[3] 컴퓨터활용능력 필기 시험에서는 이 용어를 쓴다.[4] 1의 보수 방법의 정확한 정의는 다음과 같다: 총 n개의 비트로 정수를 표현할 때, 모든 n비트가 1로 이루어진 수(2n - 1 = 111...11(2))에서 나타내고 싶은 음수의 절댓값을 뺀 수. 1의 보수 방법이라 부르는 이유가 여기에 있다.[5] 000011(2) + 111100(2) = 111111(2) = 0[6] 정의대로라면 +0을 의미한다.[7] 정의대로라면 -0을 의미한다.[8] 2의 보수 방법의 정확한 정의는 다음과 같다: 총 n개의 비트로 정수를 표현할 때, 2n = 1000...0(2)에서 나타내고 싶은 음수의 절댓값을 뺀 수. 2의 보수 방법이라 부르는 이유가 여기에 있다.[9] 2의 보수 방법으로 이진수를 구하는 다른 방법도 있다. -3을 구하는 과정으로 이 방법을 설명하겠다. 우선 이 수의 절댓값인 +3을 2진수로 표현한다. +3 = 000011 그리고 왼쪽에서부터 1이 나올 때까지 모든 0을 1로 반전시킨다. 왼쪽에서부터 가장 처음 나오는 1은 0으로 반전시키고 그 이후의 수들은 모두 반전시키지 않는다. 그렇게 나온 수가 2의 보수 방법으로 표현한 음수가 된다. 즉 000011(2)에서 밑줄친 1 이전의 0은 모두 1로, 밑줄친 1은 0으로, 그 이후는 그대로 둬서 111101(2)을 얻어 -3을 표현한다. 다른 예) -5 = -(000101(2)) = 111001(2), -10 = -(001010(2)) = 110010(2)[10] 1의 보수 방법의 정확한 정의는 다음과 같다: 총 n개의 비트로 정수를 표현할 때, 모든 n비트가 1로 이루어진 수(2n - 1 = 111...11(2))에서 나타내고 싶은 음수의 절댓값을 뺀 수. 1의 보수 방법이라 부르는 이유가 여기에 있다.[11] 000011(2) + 111100(2) = 111111(2) = 0[12] 정의대로라면 +0을 의미한다.[13] 정의대로라면 -0을 의미한다.[14] 2의 보수 방법의 정확한 정의는 다음과 같다: 총 n개의 비트로 정수를 표현할 때, 2n = 1000...0(2)에서 나타내고 싶은 음수의 절댓값을 뺀 수. 2의 보수 방법이라 부르는 이유가 여기에 있다.[15] 2의 보수 방법으로 이진수를 구하는 다른 방법도 있다. -3을 구하는 과정으로 이 방법을 설명하겠다. 우선 이 수의 절댓값인 +3을 2진수로 표현한다. +3 = 000011 그리고 왼쪽에서부터 1이 나올 때까지 모든 0을 1로 반전시킨다. 왼쪽에서부터 가장 처음 나오는 1은 0으로 반전시키고 그 이후의 수들은 모두 반전시키지 않는다. 그렇게 나온 수가 2의 보수 방법으로 표현한 음수가 된다. 즉 000011(2)에서 밑줄친 1 이전의 0은 모두 1로, 밑줄친 1은 0으로, 그 이후는 그대로 둬서 111101(2)을 얻어 -3을 표현한다. 다른 예) -5 = -(000101(2)) = 111001(2), -10 = -(001010(2)) = 110010(2)[16] 예를 들면 부호 숫자(Signed Digit) 방법으로 수를 표현하면 덧셈 연산에서 받아올림이 발생하지 않는다는 장점을 이용해서 덧셈 연산을 많이 해야 하는 회로에서 부호 숫자를 사용한 고속 연산이 가능하다.[17] 부호 절대값, 1의 보수 방법보다 표현할 수 있는 숫자가 하나 더 많은데, 2의 보수에서는 +0과 -0의 개념이 없기 때문이다.[18] 부호 절대값, 1의 보수 방법보다 표현할 수 있는 숫자가 하나 더 많은데, 2의 보수에서는 +0과 -0의 개념이 없기 때문이다.[19] 당연히 16비트, 64비트 정수에서는 성립하지 않는다(15, 63이 합성수이므로).[20] 북한에선 '류동소수점수형'이라 하는 듯하다.[21] 북한에선 '류동소수점수형'이라 하는 듯하다.[22] Not a Number의 약자로, 0 ÷ 0 같은 연산의 결과값처럼 수학적으로 계산 불가능한 값을 나타낼 때 쓰인다.[23] Not a Number의 약자로, 0 ÷ 0 같은 연산의 결과값처럼 수학적으로 계산 불가능한 값을 나타낼 때 쓰인다.[24] 총 1 + 8 + 23 = 32비트(=4바이트)[25] 총 1 + 11 + 53 = 64비트(=8바이트)[26] 표현하는 범위가 다르다[27] 10진수를 2진수로 바꾸는 과정을 의미한다.[28] 이렇게 함으로써 exp는 언제나 양수가 된다.[29] 이렇게 첫 자리 1을 생략함으로써 우리는 유효숫자를 한 자리 더 표현할 수 있게 되었다![30] 이렇게 함으로써 exp는 언제나 양수가 된다.[31] 이렇게 첫 자리 1을 생략함으로써 우리는 유효숫자를 한 자리 더 표현할 수 있게 되었다![32] 2진수를 10진수로 바꾸는 과정을 의미한다.[33] float형은 8비트로 exp를, 23비트로 frac을 나타낸다.(단정도)[34] Normalized value를 표현할 때와 다르게 정수부가 1이 아닌 0임에 주의하라![35] Normalized value를 표현할 때와 다르게 정수부가 1이 아닌 0임에 주의하라![36] 엄밀히 표현하면 앞의 수 0x0000 0000은 +0, 뒤의 수 0x8000 0000는 -0을 의미한다.[37] 0000 0000 0000 0000 0000 0000 0000 0001(2) = 0x0000 0001으로 인코딩된다.[38] 이를 gradual overflow(점진적 오버플로우)라 한다.