계산식의 자리수를 억제. 부동 소수점
컴퓨터는 숫자를 연산할 때 정수 연산과 소수 연산의 2가지로 나눠 구현합니다. 이건 '처리해야 하는 숫자의 자리수'가 매번 다르기 때문입니다.
정수는 보통 10자리 정도면 충분합니다. 대규모 기업의 회계 처리라면 10자리로도 부족하지만 그런 경우는 많지 않겠지요. 그러나 소수는 필요한 자리수가 매번 다릅니다. 옐르 들어 백만원이라면 7자리인데 여기에 할푼리가 붙으면 9자리 10자리가 됩니다.
이 경우 정수 8자리에 소수 2자리 정도면 되지만, 어떤 경우에는 정수는 2자리면 족한데 소수가 5자리 필요한 경우도 있습니다. 이들 모두를 맞추려면 13자리 가지고선 부족하며 더 많은 자리수가 필요하게 됩니다.
이렇게 만들면 항상 다 쓰지도 않는 자리수 때문에 너무 많은 공간을 할애하게 되니, 소수를 다룰 때는 부동 소수점이라는 방식을 쓰게 됐습니다. 예를 들어 123456.789라는 숫자를 다룰 경우 이렇게 데이터를 저장합니다.
123456.789의 표시 방식 | ||||||
---|---|---|---|---|---|---|
기존의 방식(고정 소수점 방식) | 123456.789 | 정수 부분 6자리 + 소수 3자리 | ||||
부동 소수점 방식 | 1.23456789 × 10 5 | 가수 부분 9자리 + 지수 1자리 |
이 방식이 어디가 좋은지는 아래 예시를 보면 잘 드러납니다. 12345678.9와 1.23456789입니다.
12345678.9의 경우 | ||||||
---|---|---|---|---|---|---|
고정 소수점 방식 | 12345678.9 | 정수 부분 8자리 + 소수 1자리 | ||||
부동 소수점 방식 | 1.23456789 × 10 8 | 가수 부분 9자리 + 지수 1자리 | ||||
1.23456789의 경우 | ||||||
고정 소수점 방식 | 1.23456789 | 정수 부분 1자리 + 소수 8자리 | ||||
부동 소수점 방식 | 1.23456789 × 10 0 | 가수 부분 9자리 + 지수 1자리 |
고정 소수점 방식으로는 1.23456789 ~ 12345678.9까지의 모든 숫자를 처리하기 위해 정수 부분 8 자리 + 소수 8 자리가 필요합니다. 하지만 부동 소수점 방식에선 가수 부분(1.23456789를 저장하는 곳)이 9자리, 지수(10의 제곱을 저장하는 자리) 1자리니까 총 10자리입니다.
다루는 값의 범위가 엄격하게 정해져 있고, 이게 변하지 않는다면 고정 소수점 방식이 나쁘지 않습니다. 오히려 편하지요. 하지만 어떤 값이 나올지 예상하지 못하는 경우가 많으며, 이 경우에는 부동 소수점이 더 작은 공간을 차지합니다.
숫자 표현은 반정밀도에서 8배정밀도까지
1980년대 무렵까지 부동 소수점은 표준이 정해지지 않고 컴퓨터 제조사가 제각각 규격을 결정했으나, 1985년에 IEEE(미국 전기 전자 학회)가 IEEE 754라는 표준(IEEE 754 -1985)을 정해 이를 따르는 식으로 구현하고 있습니다. 따라서 최근에는 특이한 요구가 없는 한 부동 소수점 포맷은 정해져 있습니다.
IEEE 754는 2008년에 개정돼(IEEE 754-2008) 여러가지 표현이 늘어나고 있습니다. 최근의 CPU는 모두 이 IEEE 754-2008에 따라서 연산을 한다고 봐도 됩니다.
그러니 여기에선 IEEE 754-2008를 바탕으로 깔고 이야기를 하겠습니다. IEEE 754-2008는 숫자의 표현에 따라 아래의 8가지 포맷을 정했습니다.
IEEE 754-2008 형식 | ||||||
---|---|---|---|---|---|---|
포맷 | 가수 부분 (용량) |
가수 부분 (자릿수) |
지수 (크기) |
지수 (범위) |
기수 | 데이터 길이 |
Binary16 (반정밀도) (FP16) |
11bit | 3 자리 | 5bit | -14 ~ + 15 | 2 | 16bit |
Binary32 (단정밀도) (FP32) |
24bit | 7 자리 | 8bit | -126 ~ +127 | 2 | 32bit |
Binary64 (배정밀도) (FP64) |
53bit | 15 자리 | 5bit | -1022 ~ + 1023 | 2 | 64bit |
Binary128 (4배정밀도) (FP128) |
113bit | 34 자리 | 15bit | -16382 ~ + 16383 | 2 | 128bit |
Binary256 (8배정밀도) (FP256) |
237bit | 71 자리 | 19bit | -262142 ~ + 262143 | 2 | 256bit |
Decimal32 (단정밀도) | 21bit | 7 자리 | 11bit | -95 ~ + 96 | 10 | 32bit |
Decimal64 (배정밀도) | 51bit | 16 자리 | 13bit | -383 ~ + 384 | 10 | 64bit |
Decimal128 (4배정밀도) | 111bit | 34 자리 | 17bit | -6143 ~ + 6144 | 10 |
128bit |
그러나 이 중 Binary16 / Binary256 / Decimal32의 3가지 형식은 교환용입니다. 교환용이란 말은 데이터를 전달하는데만 쓴다는 것으로, 예를 들어 Binary16라면 Binary32/64/128 사이의 상호 변환이 가능하면 좋고, 연산 자체를 구현할 필요는 없습니다. 실제로 최근 AI에선 Binary16의 연산을 구현하는 사례가 많아지고 있습니다.
기본이 되는 단정밀도 부동 소수점 연산. 2진수를 10진수로 변환하면 오차가 생겨남
우선 기본인 Binary32를 봅시다. 단정밀도 부동소수점에 사용하는 형식으로 그 구조는 이렇습니다.
단정밀도 부동 소수점 연산의 내부 포맷
위 표에 나온 것과 가수 부분의 크기가 다르지만, 위의 표는 가수 부호 Bit를 포함한 것입니다. 부호+23Bit니 총 24Bt가 됩니다. 그런데 부호 Bit는 0이면 양수, 1이면 음수, 그 다음은 다소 복잡합니다. 우선 지수를 봅시다. 00000000 (0)의 경우는 숫자 그 자체인 0, 혹은 비정규(0은 아니지만 한없이 0에 가까운 작은 숫자)를 가리키며, 11111111 (255)는 무한대 또는 NaN (Not a Number)을 의미합니다.
보통 숫자라면 00000001 (1) ~ 11111110 (254) 사이의 범위에 들어가는데 여기서 127은 -126~127 사이의 값을 표현합니다. 즉 2 -126 ~ 2 127 이라는 범위가 됩니다.
가수 부분은 지수가 0이 아닌 경우 최상위에 1을 세웁니다. 그럼 가수 부분은 23Bit밖에 없어도 실질적으로는 24Bt가 됩니다. 그리고 이것이 중요합니다. 23Bit에선 10진수가 6.923자리, 유효 숫자가 7자리가 되지 않습니다. 그런데 24Bit에선 7.224자리, 7자리가 되버립니다. 따라서 가수 부분 가장 앞에 1이 붙어 있으며 이것이 1에 해당됩니다. 아래 표슬 봅시다.
Binary32에서 연산 | ||||||
---|---|---|---|---|---|---|
가수 부분의 표현 | 내부에서 작업 | 값 | ||||
00000000000000000000000 10000000000000000000000 11000000000000000000000 11100000000000000000000 : 11111111111111111111111 |
100000000000000000000000 110000000000000000000000 111000000000000000000000 111100000000000000000000 : 111111111111111111111111 |
1.0 1.5 1.75 1.875 : 1.99999988079071 |
이 값은 2진수니 1.0 ~ 1.999999 ...의 범위에 들어갑니다. 물론 이는 내부 표현이며 실제로는 10진수로 내부에서 변환해 출력합니다.
Binary64 / 128 / 256은 가수 부분과 지수의 숫자가 늘어나(그 결과 가수 부분의 유효 숫자가 늘어나 취급 값의 범위가 넓어짐)고, 반대로 반대로 Binary16는 숫자가 절반 이하로 떨어져 유효 숫자와 취급 값이 모두 좁아집니다.
덧붙여서 Decimal32 / 64 / 128은 제대로 10진수 연산을 실행할 때만 씁니다. Decimal32는 아래 표대로 딱 나눠 떨어지는 숫자가 아닙니다.
Decimal32에서 연산 | ||||||
---|---|---|---|---|---|---|
가수 부분의 표현 | 내부에서 작업 | |||||
00000000000000000000000 01000000000000000000000 00100000000000000000000 00010000000000000000000 00001000000000000000000 00000100000000000000000 00000010000000000000000 00000001000000000000000 00000000100000000000000 00000000010000000000000 00000000001000000000000 00000000000100000000000 00000000000010000000000 00000000000001000000000 00000000000000100000000 00000000000000010000000 00000000000000001000000 00000000000000000100000 00000000000000000010000 00000000000000000001000 00000000000000000000100 00000000000000000000010 00000000000000000000001 |
1.5 1.25 1.125 1.0625 1.03125 1.015625 1.0078125 1.00390625 1.001953125 1.0009765625 1.00048828125 1.000244140625 1.0001220703125 1.00006103515625 1.000030517578125 1.0000152587890625 1.00000762939453125 1.000003814697265125 1.0000019073486325625 1.00000095367431628125 1.000000476837153140625 1.0000002384185765703125 1.00000011920928828516625 |
10진법은 1.1+0.1=1.2가 당연하지만, 2진법에선 이게 '1.1에 한없이 가까운 값'+'0.1에 한없이 가까운 값'이라는 계산이다보니 가끔 1.2가 되지 않는 경우가 종종 있습니다(유효 숫자 범위에 따라 달라집니다).
여기에 관련된 유명한 일화도 있습니다. 걸프 전쟁이 한참이던 1991년 2월에 미국의 패트리어트 지대공 미사일이 이라크의스커드 미사일을 요격하지 못했던 사건이 있었습니다. 그 원인은 패트리어트 미사일의 제어에 쓰던 프로그램이 내부적으로 24Bit 카운터를 써서 시간을 측정했는데, 2진수/10진수의 변환 오차가 쌓이면서 타이밍이 0.34초 어긋났기 때문입니다.
원인이 밝혀지만 시스템을 재시작해 오차 축적을 없앨 수 있지만, 근본적으로는 2진수를 기반으로 연산하면 오차를 완전히 벗어나지 못하기에, 10진수 기반의 부동소수점 포맷도 추가됐습니다.
정확도 향상의 댓가는 느린 연산 속도
그럼 왜 이렇게 많은 규격이 있는 걸까요? 정확성과 속도가 제각각 다르기 때문입니다 앞서 말한대로 단정밀도의 유효 숫자는 7자리입니다. 일반 용도에선 이걸로도 충분하나 과학 기술 계산에선 턱없이 부족합니다.
게임과 가상화폐 채굴은 단정밀도 부동소수점 연산으로도 충분합니다. 지포스는 단정밀도에 특화된 GPU입니다. 최상위 모델인 지포스 GTX 1080 Ti는 단정밀도 연산 능력이 10.8Tfops지만 배정밀도는 0.36TFlops밖에 나오지 않습니다.
특히 장시간 시뮬레이션을 실행하는 분야. 예를 들어 유체 해석과 날씨 예측처럼, 짧은 간격으로 끝없이 계산해 나가는 경우 오차의 누적이 너무 많습니다. 앞서 예로 든 패트리어트 미사일도 단정밀도 부동소수점 연산을 100시간 정도 실행하니 0.34초의 오차가 생겼습니다. 그리고 이게 미사일 요격 실패라를 결과를 낳았지요. 이를 막는 가장 쉬운 방법은 유효 숫자의자리수를 늘리는 것입니다.
배정밀도 부동 소수점 연산에서 높은 성능을 자랑하는 NVIDIA 쿼드로 GV100. 연산 능력은 배정밀도 7.4Tflops, 단정밀도14.8Tflops
단정밀도와 배정밀도를 비교하면 배정밀도가 2~4배 정도 느려집니다. 이건 처리해야 할 데이터의 양이 배로 늘어마면서, 덧셈은 2배, 곱셈은 4배가 느려졌기 때문입니다. 구체적으로 얼마가 느려지는지는 구현 방법에 따라 다릅니다. 예전의 x86처럼 64비트 연산에 32비트 연산기를 사용할 경우엔 64비트 곱셉이 32비트 곱셈을 4번 수행하고 여기에 32비트를 더해야 하니까 5배 이상 느려지기도 했습니다.
그래서 정확도는 적당한 수준이면 되니 빠른 연산이 필요한 경우에는 배정밀도가 맞지 않습니다. 이를 극단적으로 추구한 것이 3D그래픽과 Z 버퍼, 머신 러닝에 쓰이는 CNN(Convolutional Neural Network)의 처리입니다.
CNN의 경우 극단적인 상황에선 1Bit(0 또는 1)로도 어느 정도 정확도가 나옵니다. 8Bit라면 실용적인 수준의 정밀도가 확보됩니다. 이런 용도에 23Bit 가수 부분을 지닌 단정밀도 부동 소수점을 쓰는 건 낭비이며, 반정밀도 아직 활용 분야가 많습니다.
NVIDIA가 파스칼에서 반정밀도(FP16)을 지원하면서 CNN 속도를 확보했습니다. 볼타 세대에선 8비트의 텐서 코어를 탑재해 더욱 성능을 높였는데, 이는 정밀도보다도 속도가 더 중요하기 때문입니다.
결론입니다. 단정밀도와 배정밀도가 모두 있는 건 저마다 각각의 요구가 다르고, 이게 합쳐질 이유가 없기 때문입니다.