티스토리 뷰

 

 

 

 

1991년 걸프전에서 패트리어트 미사일 방어 시스템의 부동소수점 연산 오류로 인해 미군 병사 28명이 사망하고 100여 명이 부상을 입었다. 패트리어트 시스템은 내부 클럭을 이용해 미사일의 궤적을 계산하는데, 0.1초 부동소수점으로 저장하면서 이진수로 정확히 표현되지 않아 미세한 오차가 발생했고, 100시간 이상 가동되며 누적된 오차는 0.34초까지 증가해 결국 적 미사일의 위치를 잘못 예측하게 되었다. 이로 인해 요격에 실패했고 미사일이 미군 병영을 타격하며 큰 피해를 초래했다.

 

실제로 위와 같은 사건이 있었습니다. 사건의 발단은 32, 64비트로 제한된 컴퓨터에서 무한소수를 정확하게 나타내지 못하기 때문입니다.

 

해당 포스팅에서는 아래와 같은 내용을 알아보겠습니다.

  • 컴퓨터가 부동 소수점을 표현하는 방법 (과학적 표기법, 단일 정밀도 부동 소수점)
  • 사람이 부동 소수점을 보고 십진수로 변환하는 방법
  • 부동 소수점에서 오차가 발생하는 이유
  • 부동 소수점을 설계할 때 고려해야 하는 부분 (정밀도, 표현 범위)
  • 부동 소수점 오차로 인해 생기는 문제

정수

먼저 컴퓨터 메인 메모리(RAM)에 양의 정수를 저장해 봅시다. (음수는 나중에)

 
5를 저장하는 경우

0 0 0 0 0 1 0 1

 
16을 저장하는 경우

0 0 0 1 0 0 0 0

 

255를 저장하는 경우

1 1 1 1 1 1 1 1

 
양의 정수는 다음과 같이 이진수로 정확하게 나누어 떨어집니다.
그럼 5.125 같은 실수는 어떻게 저장을 할까요?
 


실수

컴퓨터는 정수와 부호 없는 정수뿐만 아니라 소수 부분을 가지는 수도 다룰 수 있어야 합니다.
수학에서는 이러한 수를 실수라고 부릅니다. 실수를 몇 가지 예시 들면

  • 3.14159265...ten(π)
  • 2.71828...ten
  • 0.0000000001ten 또는 1.0ten×109 (나노초)
  • 3,155,760,000ten 또는 3.15576ten×109 (1세기를 초로 표시한 값)

1세기를 초로 표시한 값은 32비트 부호 있는 정수로는 표현할 수 없는 값임에 주의해야 합니다.
(32비트 부호 있는 정수 범위: [−2,147,483,648 ~ 2,147,483,647]
 
마지막 두 수의 표현 방식은 과학적 표기법(scientific notation)입니다. 두 방식은 소수점의 왼쪽에는 한 자릿수만 있습니다.
과학적 표기법으로 표현된 숫자 중에서 맨 앞에 0이 나오지 않는 것을 정규화된 수(normalized number)라고 부릅니다. 일반적으로 이런 정규화된 수를 사용합니다.
 
예를 들어 1.0ten×109는 정규화된 과학적 표기법이지만,
0.1ten×10810.0ten×1010은 정규화된 과학적 표기법이 아닙니다.
 
십진수는 다음과 같지만, 이진수를 정규화된 형태로 표현하기 위해서는 소수점 왼쪽에 0이 아닌 숫자가 한자리만 나타나게 숫자를 자리이동한 후 자리이동 횟수만큼 증가시키거나 감소시킬 수 있는 기수(base)가 필요합니다.
이 조건을 만족시킬 수 있는 기수는 2밖에 없습니다.

3,155,760,000ten에서 소수점을 오른쪽으로 9칸 이동하여 정규화시키면?  3.15576ten×109

 
이런 수를 지원하는 컴퓨터 연산을 부동 소수점(Floating Point) 연산이라 부릅니다.
왜냐하면 정수에서와 달리 소수점이 고정되어 있지 않기 때문입니다.
C언어는 이러한 수를 나타내기 위해 float이라는 변수형을 사용합니다.
 
결론적으로, 과학적 표기법으로 이진수를 나타내면 다음과 같습니다.

1.xxxxxxxxxtwo×2yyyy

 
실수를 정규화된 형태인 과학적 표기법으로 나타내는 이유는 다음과 같습니다.
1. 교환의 용이성: 부동 소수점 숫자를 포함한 데이터를 다른 시스템 간에 교환할 때 표준화된 형태로 표현되므로 간단해집니다.
2. 산술 알고리즘 단순화: 숫자가 정해진 형태로 표현되기 때문에, 부동 소수점 산술 알고리즘을 단순하게 만들 수 있습니다.
3. 정밀도 증가: 불필요한 선행 0을 제거하여 한 워드 내에서 더 많은 숫자를 저장할 수 있게 되어 정밀도가 증가합니다.


단일 정밀도 부동 소수점 (Single-Precision Floating Point)

부동 소수점 표현 방식을 설계하는 사람은 소수 부분(fraction)의 크기와 지수(exponent)의 크기 사이에서 타협접을 찾아야 합니다.
왜냐하면 고정된 워드(Word) 크기를 사용하므로 하나를 증가시키면 다른 하나를 감소시켜야 하기 때문입니다.
즉, 이 문제는 정밀도 표현 범위사이 선택입니다. (좋은 설계에는 적당한 절충이 필요합니다!)

소수 부분의 크기를 증가시키면 소수 부분으로 표현할 수 있는 정밀도가 높아지고,
지수의 크기를 증가시키면 소수 부분으로 표현할 수 있는 수의 범위가 늘어납니다.

 
부동 소수점의 크기는 보통 워드 크기의 배수(32, 64,..., 32n)입니다.
 

단일 정밀도 부동소수점

부동 소수점 표준 표현은 아래 그림과 같습니다.

IEEE 754 단일 정밀도 부동 소수점 표준

 

부호 비트 (S, 1-bits)

숫자가 양수인지 음수인지를 결정하는 부호 비트입니다.
S=0이면 양수, S=1이면 음수입니다.
이 부호 비트는 계산에서 중요한 역할을 하며, 부동소수점 수를 다룰 때 숫자의 부호를 쉽게 반영할 수 있게 해 줍니다.
 

지수 (E, 8-bits)

E는 2의 거듭제곱으로 숫자를 얼마나 크게 또는 작게 만들 것인지를 나타냅니다.
지수는 이진수로 표현되며, 이를 통해 2의 거듭제곱 형태로 숫자의 크기를 조정합니다.
지수는 bias를 적용하여 저장되며, 예를 들어 단일 정밀도에서는 127의 bias를 사용합니다. 즉, 실제 지수값 E는 저장된 지수값 E에서 127을 뺀 값입니다. 이렇게 함으로써, 부호가 있는 지수를 다룰 수 있고, 음수와 양수 지수 모두를 표현할 수 있습니다.
 

가수 (F, 23-bits)

F는 소수 부분을 나타냅니다. 가수 부분은 정규화된 형태로 저장됩니다.
즉, 가수는 항상 1.으로 시작하고, 그 뒤에 소수점 이하의 숫자들이 따라오는 형태입니다. 그래서 1.F로 저장되는 경우가 많습니다.
예를 들어, F = 0.101이라면 실제로 저장되는 값은 1.101입니다. 이는 부동소수점의 정규화 규칙인 과학적 표기법에 따라, 숫자를 효율적으로 표현할 수 있도록 만들어집니다.
 
따라서 단일 정밀도 부동 소수점 수는 다음과 같은 형태를 가집니다.

(1)S×2E×F 

 
이렇게 선택된 지수와 소수 부분의 크기는 컴퓨터가 매우 큰 표현 범위를 가집니다. 어느 정도 범위를 가질 거라고 예상하시나요?
 
무려 2.0ten×1038에서 2.0ten×1038까지 컴퓨터에서 표현될 수 있습니다.
 

2128에 해당하는 십진수 값을 찾아봅시다.

지수가 255라면 255 - 127(bias) = 128이며 이는 2128을 의미합니다.
2x=10y가 있고, 여기에 log10을 씌우면
xlog102=y
log102=0.3010이며, x128을 대입하면
따라서 128×0.301=38.5입니다.
 
물론 이 보다 더 큰 범위를 가지는 수를 표현하고 싶을 수 있습니다.
이 경우 해당 표현 형식을 사용하면 오버플로(overflow) 또는 언더플로(underflow)가 발생합니다.
따라서 2배 정밀도(double precision) 부동 소수점 연산을 사용해야 합니다. 앞서 설명한 표현 형식은 단일 정밀도(single precision) 부동 소수점 연산이라고 합니다.

 


부동 소수점을 십진수로 변환하기

주어진 부동 소수점을 십진수로 변환할 수 있어야 합니다. 한번 변환해 봅시다.

다음과 같은 단일 정밀도 부동 소수점이 주어졌다고 가정합시다.

0 10000001 01001000 00000000 000000

맨 앞 비트는 부호비트를 의미하고 0이므로 양수입니다.

 

그다음 지수는 20+27=129입니다. 즉, Bias(127)를 적용하면 129 - 127 = 2이므로 소수점을 왼쪽으로 두 번 옮겼다는 의미입니다.

따라서 1이 생략된 가수 부분 1.01001000 00000000 000000에서 소수점을 오른쪽으로 다시 두 번 복귀시키면​

101.001000 00000000 000000 형태입니다. 따라서 정수 부분은 101이고 이는 십진수로 5입니다.

 

그다음 소수 부분은 앞에서부터 23=18=0.125입니다.

따라서 정수 부분과 소수 부분을 합치면 5.125입니다.

 

매번 이렇게 계산하지 않고, 공식에 적용하면 됩니다.

 

IEEE 754 단일정밀도 부동소수점을 십진수로 변환하는 공식

V=(1)Sign×(1+Mentissa)×2Exponent127 

5.125를 해당 공식에 대입해 봅시다.

Sign = 0

Mentissa = 01001000 00000000 000000 = 22+25=0.28125

Exponent = 129

따라서 V=10×(1+0.28125)×2(129127)=1×1.28125×4=5.125 라는 사실을 알 수 있습니다!


이번에는 십진수를 단일 정밀도 부동소수점 형태로 표현해 봅시다.

 

5.125단일 정밀도 부동소수점으로 저장해 봅시다.

양수이므로 부호(S, 1bit)는 0입니다.
 
먼저 5.125를 이진수로 표현하면 101.001입니다.
컴퓨터 과학적 표기법으로 나타내면 1.01001이고, 소수점을 왼쪽으로 두 번 옮겼습니다.
따라서 지수 값(E, 8bit)127+2=129이고 이를 이진수로 변환하면 10000001입니다.
 
가수는(F, 23bit)는 01001입니다.
 
최종적으로, 5.125를 단일 정밀도 부동 소수점으로 표현하면 다음과 같습니다.
0 10000001 01001000 00000000 000000
 

1.1을 단일 정밀도 부동소수점으로 저장해 봅시다.

양수이므로 부호(S, 1bit)는 0입니다.
 
먼저 1.1을 이진수로 표현하기 위해 진법 변환을 해봅시다.
정수 부분은 1입니다. 
소수 부분을 변환하면 다음과 같이 00011을 반복하는 순환소수가 발생합니다.
0.1×2=0.2 → 0
0.2×2=0.4 → 0
0.4×2=0.8 → 0
0.8×2=1.6 → 1
0.6×2=1.2 → 1
0.2×2=0.2 → 0 (반복)순환소수(무한소수) 발생
 
이를 컴퓨터 과학적 표기법으로 나타내면
소수점의 이동은 없었으므로 지수(E)는 127ten=01111111two입니다. 
1.000110001100011...00011 와 같이 무한하게 반복합니다.

 

따라서 1.1을 단일 정밀도 부동소수점으로 표현하면 아래와 같습니다.
0 0111111100011000110001100011000...(00011 반복해야 하지만 잘림)

 

하지만 우리의 컴퓨터는 32비트 또는 64비트이고, 1.1을 32비트 컴퓨터 메모리에 저장해 봅시다.

0 0 1 1 1 1 1 1
1 0 0 0 1 1 0 0
0 1 1 0 0 0 1 1
0 0 0 1 1 0 0 0
1 1 0 0 0 1 1 0 ...

 
다음과 같이 순환소수를 32비트 또는 64비트 컴퓨터에 온전히 표현할 수 없기 때문에 오차가 발생합니다.
(순환소수를 가수비트로 나타낸 빨간색 부분이 제한된 메모리 공간(32비트 또는 64비트)으로 인해 잘리므로 오차가 발생)

개발자 도구를 켜서 콘솔창에 (1.1 + 0.1)를 해보시면 1.2000000000000002가 나오는 사실을 볼 수 있습니다.
반대로 순환소수가 발생하지 않도록 (1.1 + 0.15)를 해보시면 정확하게 1.25가 나오는 사실을 볼 수 있습니다.

 

따라서, 부동소수점은 오차가 발생할 수 있습니다.

 

100.1을 단일 정밀도 부동소수점으로 저장해 봅시다.

양수이므로 부호(S, 1bit)는 0입니다.

 

100.1을 이진수로 변환하면 다음과 같습니다.
100.1 = 1100100.0001100011...(소수 부분은 00011이 반복)

 

이를 컴퓨터 과학적 표기법으로 나타내면
소수점을 왼쪽으로 6번 이동했으므로 지수(E, 8bit)는 127+6=133 이진수로 변환하면 10000101입니다.

 

가수(F, 23bit)는 소수점 이하를 나타내므로, 23비트까지 채우면 다음과 같습니다.

따라서 100.1을 단일 정밀도 부동소수점으로 표현하면
0 10000101 00011000110001100011000

 

100000.1을 단일 정밀도 부동소수점으로 저장해 봅시다.

양수이므로 부호(S, 1bit)는 0입니다.

 

100000.1을 이진수로 변환하면 다음과 같습니다.
100000.1 = 11000011010100000. 0001100011...(소수 부분은 00011이 반복)

컴퓨터 과학적 표기법으로 나타내면
소수점을 왼쪽으로 16번 이동했으므로 지수(E, 8bit)는 127+16=143, 이진수로 변환하면 10001111입니다.

 

가수(F, 23bit)는 소수점 이하를 나타내므로, 23비트까지 채우면 다음과 같습니다.

따라서 100000.1을 단일 정밀도 부동소수점으로 표현하면
0 10001111 00011000110001100011000

 

엔지니어는 부동소수점을 설계할 때 정밀도와 표현범위를 고려해야 한다!

          1.1 = 01111111 00011000110001100011000

      100.1 = 0 10000101 00011000110001100011000

100000.1 = 0 10001111 00011000110001100011000

 

1.1의 다음 수는 무엇일까요? 가수 부분에 1을 올리면 되겠죠? 01111111 00011000110001100011001 

100.1 다음 수는? 0 10000101  00011000110001100011001 

100000.1 다음 수는? 0 10001111 00011000110001100011001

 

이 수를 십진수로 어떻게 나타내죠?

위에서 진행한 V=(1)Sign×(1+Mentissa)×2Exponent127공식에 대입하면 됩니다!

근데 공식에 대입하기 전에 변경되는 가수 부분인 Mentissa를 기준으로 생각해 봅시다.

 

1.1과 1.1 다음에 나타낼 수의 차이점은 Mentissa에서 맨 마지막 비트인 223이 1이냐 1이 아니냐의 차이입니다. 

즉 1.1 Mentissa 값에 Mentissa만 223=0.00000012 더해주면 됩니다.

 

이를 공식에 대입해서 나타내봅시다.

1.1=10×(1+Mentissa)×20

1.1 다음 수=10×(1+Mentissa+0.00000012)×20

 

100.1=10×(1+Mentissa)×26

100.1 다음 수=10×(1+Mentissa+0.00000012)×26

 

100000.1=10×(1+Mentissa)×216

100000.1 다음 수=10×(1+Mentissa+0.00000012)×216

 

혹시 눈치채셨나요?

1.1, 100.1, 100000.1의 소수점 부분이 0.1로 같지만 지수 부분, 즉 정수 부분이 다르기 때문에 다음에 나타내는 수의 간격차이가 생깁니다.

1.1의 경우는 똑같은 mentissa값을 더해 20을 곱하지만, 100.1은26을 곱하고, 100000.1은 216을 곱합니다.

 

이 의미는 다음과 같습니다.

1.1과 1.1 다음수의 간격은 0.00000012×20 차이.

100.1과 100.1 다음수의 간격은 $0.00000012 \times 2^6$ 차이,

1000001과 100000.1 다음수의 간격은  $0.00000012 \times 2^{16}$ 차이

 

따라서 부동소수점에서 지수 부분과 가수 부분의 선택이 정밀도와 표현 범위사이에 많은 영향을 미치기 때문에 엔지니어는 이 부분을 정밀하게 고려해야 합니다.


부동 소수점 오차로 인해 생기는 문제

장반경이 6,378,137m인 지구를 1cm 간격으로 표현하기 힘들다.

지구의 장반경이 약 6,378,137m이므로 이를 1cm 간격으로 표현하려면 매우 높은 정밀도가 필요합니다.

 

지구를 부동소수점으로 표현할 경우

IEEE 754 단일 정밀도 부동소수점(float, 32비트)은 7자리 십진수를 정밀하게 표현할 수 있습니다. 그러나 지구의 장반경 크기(6,378,137m)를 고려하면 소수점 이하 1cm 단위까지 정확하게 표현하려면 정밀도가 부족합니다.

6,378,137.01 

위 숫자를 단일 정밀도로 저장하려 하면 근삿값이 저장되며, 미세한 차이가 발생합니다. 이로 인해 1cm 이하의 차이를 표현할 수 없고, 지형 모델링이나 위성 측량과 같은 정밀한 계산에서 오차가 누적될 수 있습니다.

 

실제로 6,378,137.x 형태를 다음과 같이 출력해 보면 6378137.0 다음 수는 6378137.5라는 사실을 알 수 있습니다.

float f0 = 6_378_137.0f; // 6378137.0
float f1 = 6_378_137.1f; // 6378137.0
float f2 = 6_378_137.2f; // 6378137.0
float f3 = 6_378_137.3f; // 6378137.5
float f4 = 6_378_137.4f; // 6378137.5
float f5 = 6_378_137.5f; // 6378137.5
float f6 = 6_378_137.6f; // 6378137.5

이 문제를 해결하기 위해 더 높은 정밀도의 부동소수점(64비트 double precision) 또는 고정소수점 방식을 활용해야 합니다. 또한, RTC(Relative To Center) 좌표 변환을 사용하여 상대적인 위치를 기준으로 좌표를 표현하면 좌표의 범위를 낮춰 더욱 높은 정밀도를 확보할 수 있습니다.

double d0 = 6_378_137.0; // 6378137.0
double d1 = 6_378_137.1; // 6378137.1
double d2 = 6_378_137.2; // 6378137.2
double d3 = 6_378_137.3; // 6378137.3
double d4 = 6_378_137.4; // 6378137.4
double d5 = 6_378_137.5; // 6378137.5
double d6 = 6_378_137.6; // 6378137.6

어? double로 출력하니까 값이 제대로 나오는데요?

하지만 문제점은 대부분의 GPU는 32비트만 지원하고, float 연산에 대해 최적화 되어있습니다. (최근에는 GPU가 64비트도 지원합니다. 하지만 여전히 float이 최적화 유리 및 메모리 측 많은 이점)

따라서 좌표변환에 대한 연산은 주로 GPU의 Vertex Shader에서 32비트 float연산을 하는게 유리합니다.

 

엥? 637,813,700cm를 정수로 표현하면 되는 거 아니야? 

다음과 같이 생각할 수 있습니다. 실제로 정수를 사용하면 637,813,700cm와 같은 값이 32비트 정수(int)로 충분히 저장될 수 있습니다. 하지만, 좌표 변환과 연산의 번거로움 그리고 가상 지구를 컴퓨터로 표현할 때 행렬연산을 진행하는데 637,813,700 범위의 절댓값 정수를 가지고 연산할 경우 오버플로 또는 언더플로가 발생할 수 있습니다.

 

예를 들어, 위경도(GPS) 좌표는 37.5665, 127.9780과 같이 소수점을 포함하는 실수 값으로 표현되므로, 변환 과정에서 float 또는 double이 필요합니다.

또한, 거리 계산과 삼각함수 연산(sin, cos, tan)에도 부동소수점이 필수적이며, 정수형만 사용할 경우 계산이 불편해집니다.

특히, 지구는 구형(Sphere)이므로 ECEF(Earth-Centered, Earth-Fixed) 좌표 변환과 같은 작업에서 소수점 연산이 필요하며, 단순한 직선거리 계산을 넘어 곡면 좌표 변환을 수행할 때도 부동소수점이 필요합니다.


2배 정밀도 부동소수점(Double Precision Floating Point)

2배 정밀도 부동소수점 수는 다음과 같은 형태를 가집니다.

IEEE 754 2배 정밀도 부동 소수점 표준

2배 정밀도 표현법은 2.0ten×10308 2.0ten×10308까지의 수를 표현합니다.
이 처럼 2배 정밀도 연산은 지수의 범위를 크게 해 주기도 하지만, 주 된 장점은 더 큰 유효자리를 제공하여 정밀도를 높인다는 것입니다.

 

 

작성 중

고정 소수점(Fixed Point)

실수를 과학적 표기법으로 나타내지 않고 표현 할 수 있습니다.
이때 사용하는 방식이 고정 소수점(Fixed Point) 연산입니다.

 

고정 소수점(fixed point)은 정수를 표현하는 비트와 소수를 표현하는 비트수를 미리 정하고(고정) 해당 비트만을 활용하여 실수를 표현합니다.

  • 처음 1비트는 sign(부호)을 나타냅니다. 양수는 0, 음수는 1입니다.
  • 다음 15비트는 integer part(정수부)를 나타냅니다.
  • 다음 16비트는 fractional part(소수부)를 나타냅니다.
  • 그리고 정수부와 소수부의 경계를 소수점의 위치라고 생각하고 2진수로 변환된 수를 그대로 넣으면 됩니다.
  • 마지막으로 남는 자리는 모두 0으로 채우면 됩니다.

 

작성 중

 

부동 소수점과 고정 소수점은정밀도, 범위, 성능 면에서 차이가 있습니다.


참고