1. 레거시 부동소수점 MBF

컴퓨터에서 쓰이는 2진법 기반의 부동소수점이라는 개념이야 컴공· 전산에서 기본 중의 기본에 속하는 내용이며 본인 역시 이에 대해서 거의 7년 전에 글을 한번 쓴 적이 있다.
본인은 GWBASIC으로 프로그래밍에 처음 발을 내디딘 세대이다. 그런데 베이직이 PC와는 따로 노는 고유한 부동소수점 체계를 갖춘 언어였다는 사실을 30대 나이가 될 때까지 전혀 모르고 있었다.

쉽게 말해 같은 컴퓨터에서 실행한 다음 프로그램의 실행 결과가 GWBASIC과 QuickBasic이 서로 동일하지 않다는 것이다. 참고로 MKS$, MKI$는 해당 숫자들의 binary representation을 문자열 형태로 되돌리는 저수준 함수이다. C++라면 reinterpret_cast<char *>(&num) 한 방이면 끝났을 일이다.

10 INPUT A!
20 IF ABS(A!)<.01 THEN END
30 C$ = MKS$(A!)
40 FOR I = 1 TO 4: PRINT ASC(MID$(C$, I, 1)): NEXT
50 GOTO 10

사용자 삽입 이미지

하긴, 옛날에 베이직은 DEFINT A-Z 같은 걸 하지 않으면 변수의 기본 자료형이 정수가 아니라 실수였다. 언어를 설계할 때 성능보다 인간적인 면모를 더 추구해서 그렇다. (5/3을 구하면 매정하게 1이 아니라 알아서 1.6666..이 나오게..) 그러니 구조적으로 실수를 지원하는 건 필수였다.

때는 무려 1975년, 빌 게이츠가 폴 앨런과 함께 알테어 베이직을 개발하던 시절에 동료들과 함께 뚝딱 해서 2진법 기반의 부동소수점 표기 방식을 만든 게 Microsoft Binary Format, 일명 MBF라는 스펙이 됐다. 32비트와 64비트 두 형태로 말이다.
이 부동소수점은 알테어뿐만 아니라 BASICA, GWBASIC 등 온갖 플랫폼에서 돌아가는 베이직 인터프리터에 두루 쓰이기 시작했다. 지금도 인터넷에 굴러다니는 GWBASIC은 IEEE754가 아닌 MBF 고유 방식으로 부동소수점을 처리한다.

그랬는데 훗날 1984년경에 IEEE754라고 공신력이 더 높은 표준이 등장하면서 판도가 급격히 그쪽으로 기울었다. 게다가 PC에서는 인텔 80x87이라고 오늘날로 치면 하드웨어 가속에 해당하는 수치 연산 보조 프로세서(코프로세서)도 응당 IEEE754를 기반으로 만들어졌다.

마소는 일찍부터 자체적인 부동소수점 포맷을 먼저 제정해서 이를 퍼뜨려 왔지만 이런 시국에서는 자기도 대세를 거스를 수 없게 되었다. GWBASIC의 후신인 QuickBasic도 80년대 중반에 나왔던 1, 2까지는 MBF를 사용했지만 3.0부터는 IEEE 방식으로 갈아탔다. 그 대신 기존 MBF 방식은 별도의 옵션을 줬을 때에만 지원하게 동작이 바뀌었다. (/MBF) MBF 형태로 저장된 부동소수점을 읽어들이는 레거시 프로그램들과의 호환성도 중요하니까 말이다.

그럼 IEEE와 MBF는 어떤 차이가 있었는가? 몇 가지가 있다.
똑같은 32비트 또는 64비트 공간에다 지수와 유효숫자와 부호 비트를 어느 순서대로 어떻게 분배할지 문제는 한 마디로 그냥 정하기 나름이고 대동소이하다. 마치 철도 궤간을 정하는 문제와 비슷하다.

수 전체의 부호 1비트는 IEEE나 MBF든 공통일 수밖에 없고, 32비트의 경우는 지수 8비트, 유효숫자(mantissa) 23비트라는 비율 역시 동일했다. 다만,

(1) IEEE는 2의 보수 기반인 정수의 관행을 존중해서 부호 비트가 수 전체의 최상위 비트에 있는 반면, MBF는 지수와 유효숫자 사이에 존재했다. 다시 말해 mantissa의 최상위 비트에 있는 셈이다. 이렇게 배치를 함으로써 MBF는 IEEE와는 달리 지수와 유효숫자가 딱 8비트와 24비트로 byte padding이 맞춰지게 했다.

(2) 64비트 실수의 경우 이 비율도 달라진다. IEEE는 지수의 공간도 딱 3비트 더 늘어서 11비트이지만, MBF는 여전히 8비트이다. 그래서 32비트 single 실수를 쓰다가 64비트 double 실수를 쓰면 정밀도는 왕창 심지어 IEEE보다도 더 올라가지만 수의 표현 가능 자리수가 늘어나지는 않는다. 그 대신 바이트 경계는 여전히 1:7 비율로 지켜진다.

(3) 다음으로, MBF는 IEEE처럼 denormal number나 NaN, 무한대/무한소 같은 개념도 없다. denormal이야 숫자 표현과 관련된 내부 디테일이니 그렇다 치더라도 베이직 언어로 수학 함수를 사용하면서 NaN이나 무한대/무한소 같은 걸 접한 경험은 없다. 그런 숫자가 생성될 상황이라면 그냥 "Illegal function call" 에러가 나고 말지.
어쩐지 이런 것들은 본인이 훗날 C/C++로 갈아타면서 처음으로 접했다. 이게 엄밀히 말하면 언어 차이가 아니라 이런 부동소수점 표현 방식 때문에 생긴 차이점이다.

세계적으로 문자들은 언어와 문화권마다 제각각이지만 아라비아 숫자만은 세계 공통이다. 컴퓨터 세계도 사정이 얼추 비슷했는데 그나마 유니코드라는 규격 덕분에 동일한 문자는 세계 어디서나 동일한 방식으로 통용 가능해졌다. 그에 반해 숫자가 부동소수점 한정으로 표현 방식이 파편화돼 있었다는 건 개인적으로 무척 흥미롭게 와 닿는다.

C, 파스칼 같은 언어 이름은 함수 호출 규약 명칭에 등장하는데 베이직은 MBF라는 레거시를 보유하고 있구나. IEEE754의 등장 이전에는 MBF 말고 다른 부동소수점 표현 방식은 존재하지 않았나 하는 의문이 남으며, 파스칼에만 있던 6바이트 실수가 규격이 어떠했는지도 다시 보게 된다. 스펙을 검색해 보니 파스칼도 지수부는 8비트이고 나머지가 부호부(1비트)와 가수부(39비트)이다.

2. MOTOR의 정체는?

이 블로그에서 GWBASIC에 대한 추억들 중에서 지금까지 이걸 거론한 적은 없었던 것 같다.
GWBASIC의 대화식 환경에는 코딩 중에 자주 사용하는 키워드들을 곧바로 입력하는 일종의 키매크로가 있었다. F1부터 F10까지 기능키에 배당된 매크로는 LIST, RUN, LOAD...의 순으로 화면 밑줄에 표시되었으며 KEY라는 키워드(?)를 이용해 사용자가 재정의도 할 수 있었다. 후대의 QuickBasic 계열에서는 없어지기도 할 법도 한 키워드인데 KEY에 그 기능만 있는 건 아니기 때문에 없어지지 않고 남아 있긴 하다.

그리고 매크로가 거기에만 있는 게 아니라 Alt+알파벳에도 있었다. A부터 Z 중 J, Q, Y, Z를 제외한 나머지 22개 알파벳에는 AUTO, BSAVE, COLOR ... WIDTH, XOR까지.. 키워드가 즉시 입력되었다. 이 키워드들은 딱히 재정의 가능하지 않았다. RUN과 SCREEN은 Alt에도 있고 F 기능키에도 있었다. (후자는 엔터까지 자동으로 입력된다는 차이가 있음)

그런데 본인이 주목한 것은 M 자리에 배당되어 있던 MOTOR라는 단어였다. 이거 도대체 뭘까? 경험상 숫자 인자를 하나 받는 것 같던데 도대체 하는 일이 뭘까? 두툼한 GWBASIC 매뉴얼/키워드 레퍼런스를 뒤져봐도 의외로 딱히 제대로 설명돼 있지 않았다. 그러니 궁금증은 더욱 커질 수밖에 없었다.

이 역시 전세계에 존재하거나 존재했던 모든 것들에 대한 정보가 손끝 하나로 검색되는 세상이 온 뒤에야 그게 그런 용도였다는 것을 뒤늦게 알 수 있었다.
MOTOR는 카세트 테이프 장치의 헤더를 올리거나 내리는 명령문이었다. 0부터 255 사이의 숫자를 인자로 받긴 하는데 실질적인 의미는 그냥 zero냐 non-zero냐, 쉽게 말해 그냥 bool이었다. 카세트 테이프가 퇴출된 16비트 이상의 IBM-PC급용 베이직에서는 이 명령은 구현되지 않고 아무 동작도 안 하는 레거시 잉여가 되었다.

옛날에 카세트 테이프에다 소스 코드 저장을 SAVE"FILE" 한 뒤 '녹음' 버튼을 눌러서 쭈루룩 하고, 불러오려면 저장되었던 위치로 정확하게 되감기를 하고 LOAD"FILE"한 뒤, '재생' 버튼을 눌러서 했다던데.. MOTOR는 그런 호랑이 담배 피우던 시절에나 유의미한 기능을 했다는 뜻 되겠다.

그런데 왜 이런 잉여가 한때에는 그 시절에는 자주 쓰이기라도 했는지 Alt+M 매크로에 떡 등재돼 있었다. 현실에서는 모터 따위보다는 MOD 연산자 또는 부분문자열을 구하는 MID$ 함수가 훨씬 더 자주 쓰일 텐데 말이다. 그러고 보니 실제로 Alt+M에 MOTOR 대신 쿨하게 MID$가 배당돼 있던 GWBASIC 구현체가 있기도 했던 것 같다. 베이직은 바리에이션 구현체가 워낙 많으니.. 아니면 그건 그냥 내 기억력의 한계로 인한 착각이었는지는 모르겠다.

※ 기타

(1)
이 외에도 GWBASIC은 그러고 보니 소스 코드의 저장도 고유 방식으로 했고 심지어 후대의 QuickBasic에도 비슷한 관행이 있었다(디폴트 옵션). 베이직 언어들은 그 옛날에도 일종의 가상 기계나 독자적인 개발 환경까지 다 짬뽕으로 추구했던 것 같다.
비주얼 베이직의 중후반대(4정도?) 넘어가서 COM 기반의 BSTR 방식으로 갈아타기 전에는 베이직은 문자열도 자기만의 독자적인 이중 포인터 참조 방식으로 구현돼 있었다. 일단 null-terminate 방식이 아니기 때문에 C와는 다름. 이것도 아마 MBF만큼이나 역사가 왕창 오래 된 독자적인 관행이 아닐까 싶다. (문자열에 대해서도 옛날에 한번 글을 쓴 적이 있다.)

(2)
난 C/C++ 파스칼 같은 타 언어로는 도스에서 텍스트 모드에서 색깔을 바꾸고 표준 VGA 그래픽, 특히 mode 13h를 바꾸는 코드를 작성해 본 적이 없다. 베이직에서는 COLOR 내지 SCREEN으로 곧장 됐을 일이 타 언어에서는 표준 라이브러리에서 지원해 주지 않았기 때문이다.
GWBASIC에서 Q(uick)Basic 계열로 바뀌면서 정말 좋은 것 중 하나가 본격적인 VGA 그래픽이 지원된다는 것이었는데, 16진수를 10진수로 바꿔 버릴 생각을 어째 했나 모르겠다. 실제로는 0x13인데 그걸 그냥 13만 써도 되게..;; 그것까지 초보자를 배려한 것이었나 궁금해진다. 그 초보자가 숙련자로 등급이 바뀌는 순간부터 문화 충격을 경험할지도 모르는데..

(3)
베이직이라는 언어 자체는 다트머스 대학의 컴공 교수가 고안한 것이지만, 저런 구현체는 빌 게이츠 같은 괴짜가 아니면 생각해 낼 사람이 별로 없을 물건이다.마소에서는 처음에는 다양한 8/16비트 컴퓨터를 대상으로 베이직 인터프리터를 개발했지만, 사실 IBM PC용으로는 베이직 컴파일러도 DOS 1980년대부터 만들어 오고 있었다.
그래서 Quick이라는 브랜드를 붙여서 QuickBasic 1.0을 1985년에 내놓았다. 이때 퀵베이직은 지원하는 문법은 GWBASIC과 별 차이가 없지만 대화식 환경이 아닌 명령줄에서 컴파일 + EXE 생성만 가능한 베이직일 뿐이었다.

그러다가 1년 주기로 버전 2와 3을 내놓으면서 기존의 구닥다리 행번호 위주가 아닌 구조화 문법이 차근차근 도입되었다. 베이직이라는 언어가 이때(1980년대 중반) 1차로 마개조된 셈이다. 그리고 4.0에 와서야 비로소 함수의 재귀호출이 가능해지고, 즉석 문법 체크와 실행이 지원되는 IDE가 추가되었다. 사실, IDE 자체는 2에서부터 도입됐고 그때 이미 퀵라이브러리도 도입됐다고 하지만, 그때는 지금과 같은 IDE가 아니었다.

그 뒤 1988년 가을에 출시된 QB 4.5가 장수만세 안정판이 되었다. 퀵베이직은 1990년에 어쩐 일인지 버전 5와 6을 건너뛰고 QuickBasic Extended 내지 MS Basic PDS (전문 개발 시스템)이라는 이름으로 7과 7.1 버전까지 개발된 뒤, Visual이라는 브랜드로 바뀌었으며 이때부터 플랫폼도 Window로 바뀌었다. 5, 6을 건너뛴 이유는 퀵베보다 먼저 개발되어 온 그 전신 컴파일러의 버전 번호를 맞췄기 때문이다. (참고로 Visual C++도 IDE의 버전보다 컴파일러의 버전이 더 높음. 전신인 MS C 의 버전을 계승하기 때문이다.)

이 와중에 1991년에 출시된 MS-DOS 5.0에서는 QuickBasic에서 컴파일 기능을 떼어낸 QBasic이라는 물건을 만들고, 이 엔진으로 MS-DOS 4.0까지 내장하고 있던 GWBASIC과, EDLIN 텍스트 에디터를 동시에 대체했다. 무척 흥미로운 점이다. MS-DOS가 전체 화면 형태로 제공하던 유틸리티는 4.0에서 도입됐던 DOS Shell 이후로 이게 둘째가 아닌가 싶다.

Posted by 사무엘

2017/04/28 08:38 2017/04/28 08:38
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1354

컴퓨터 구조를 공부하면서 배우는 기본 개념 중 하나는, ‘컴퓨터 내부에서 숫자가 표현되는 원리’이다.
부호 있는 정수는 소위 말하는 ‘2의 보수’ 형태로 표현되는데, 이것은 모든 비트가 1인 숫자는 -1이 되는 형태이다. 이런 방식을 쓰는 이유는 연산 회로를 설계할 때, 뺄셈을 덧셈의 변형만으로 매우 손쉽게 구현할 수 있는 체계이기 때문이다.

베이직 언어는 다른 언어들과는 달리 TRUE의 값이 -1인데, 그 이유가 바로 이런 컴퓨터의 구조와 관련이 있다. 베이직은 비트 연산자와 논리 연산자의 구분이 없기 때문이다.
0 아니면 1밖에 모르는 디지털 컴퓨터는 연속이나 무한 같은 개념을 표현할 수 없다. 숫자도 정수만 다루는 데 익숙하다. 그러나 현실 세계에서 발생하는 문제를 해결하려면 소숫점이 동반된 실수를 다뤄야 할 일도 매우 자주 발생한다.

소수를 표현하는 가장 간단한 방법은 소위 ‘고정소수점’이다. 가령 32비트 고정소수점의 경우, 정수와 완전히 똑같은 방법으로 숫자를 표현하되 실제로는 그 수의 의미를 정수를 16비트 크기만큼(65536) 나눈 것으로 인식하는 것이다. 즉, 수의 정밀도만 1이 아닌 1/65536으로 기계적으로 높아지는 셈.

고정소수점은 일단 덧셈· 뺄셈· 비교 연산을 정수와 완전히 동일한 방식으로 할 수 있어서 처리 속도가 매우 빠르며, 곱셈과 나눗셈을 할 때만 약간 주의해서 자릿수 정돈을 하면 되니 편하다. 실제로 일부 벡터 그래픽이나 글꼴 쪽 분야에서는 이런 고정소수점 방식이 잘 쓰이고 있다. 그러나 표현 가능한 수의 범위가 매우 심각하게 제한을 받기 때문에 범용성은 떨어진다.

자리수의 제약을 받지 않고 소수점을 좀더 자유롭게 표현하려면, 유효숫자와 자릿수를 따로 둘 필요가 있다. 그게 훨씬 더 실용적이다. 부동(floating)이라는 개념이 여기에서서 나왔다. 제일 큰 자리수의 숫자가 무엇이며 그 뒤에 0이 몇 개 붙느냐가 중요한 것이다.

이런 체계에서는 10.5에서 0.5는 제대로 표현할 수 있지만, 10000000.5에서 0.5는 손실될 가능성이 커진다. 그리고 한 숫자를 결국 두 수의 조합으로 표현해야 하므로, 각종 연산이 단순 정수보다 훨씬 더 느리고 힘들어진다.

옛날에는 이런 부동소수점 계산을 하드웨어 회로 차원에서 바로 해 주는 코(보조)프로세서가 별도로 존재했다. fdiv, fmul 같은 인스트럭션. 심지어는 제곱근이나 삼각함수 값까지 바로 구하는 명령이 있다!
하지만 시중에는 그런 게 없는 컴퓨터도 있다는 얘기이기 때문에, 1990년대에 상업용으로 쓰이던 어지간한 컴파일러들을 보면 소프트웨어적으로 부동소수점 계산을 흉내 내는(엄청 느리지만-_-) 코드를 추가할지를 지정하는 옵션도 있었다.

그런데 부동소수점을 표현하는 방식 자체가 통일돼 있어야 그 기준에 따라 코프로세서를 만들든지 말든지 할 수 있을 것이다. 그 표준 규격이 바로 IEEE754이다. 1985년에 제정되었다.

이 규격은 좀 정밀도가 떨어지지만 처리 속도가 더 빠른 32비트와, 용량이 넉넉하지만 역시 속도의 압박이 있는 64비트로 나뉜다. CPU 단위가 16비트이고 부동소수점을 소프트웨어적으로 처리하는 게 보편적이던 옛날에는, 아예 컴파일러 재량으로 소프트웨어적으로 구현한 48비트(6바이트-_-.. 파스칼에서 Real) 실수와 80비트(C/C++에서 long double) 실수도 존재하였으나 지금은 완전히 흑역사가 되었다. 요즘은 화면 픽셀 크기도 24비트는 존재하지 않으며 32비트이다. 죄다 컴퓨터가 처리하기 편한 단위로 그냥 확장된 듯하다.

제일 간단하게 말하자면, 32비트 실수에서는 1비트는 이 수의 부호를, 다음 8비트는 지수를, 다음 나머지 23비트는 유효숫자를 나타낸다. 64비트 실수에서는 그 비율이 1:11:52이다.
컴퓨터가 사용하는 부동소수점의 지수의 밑은 너무 당연한 말이지만 10이 아니라 2이다. 따라서 2의 거듭제곱의 역수가 아닌 모든 소수들은 끝자리가 잘린 ‘순환소수’가 된다! 0.25, 0.125, 0.5 같은 수가 아닌 다른 모든 수들.. 0.1, 0.2, 0.3 이런 것들은 화면에서는 적당하게 근사되어 표현되었다 할지라도 실제로는 그 수의 100% 정확한 형태로 저장되지 않은 셈이다. 부동소수점의 한계를 분명히 알고 있어야 한다.

어쨌거나.. 2^23과 2^52에다 base가 10인 로그를 씌워 보면 32비트 실수의 유효숫자 정밀도는 약 7자리, 그리고 64비트 실수의 정밀도는 약 15자리라는 걸 알 수 있다. 그리고 표현할 수 있는 자리수의 범위는 32비트는 약 38자리, 그리고 후자는 약 308자리이다(비트가 3개 더 늘어서 2^3인 8배가 더 늘었으므로). 소수점 밑으로도 그만치 내려갈 수 있다.

까놓고 말해 이런 공용체를 통해 부동소수점을 쉽게 해부할 수 있다.

union REAL {
   float fValue;
   struct {
      unsigned sign: 1; //부호
      unsigned expo: 8; //지수부
      unsigned mantissa: 23; //가수부
   };
};

공용체와 비트 필드가 존재하는 C/C++ 만만세. ㄲㄲ
단, 우리가 쓰는 인텔 CPU는 little endian이므로 sign, expo, mantissa 순이 아니라 반대로 mantissa, expo, sign으로 멤버 순서만 바꿔 주면 된다. 쉽죠?

그런데 지수부와 가수부 사이에 아무런 제약을 가하지 않을 경우 동일한 숫자를 여러 방법으로 표현할 수 있게 되어 문제가 생긴다. 4를 2의 2승, 4의 1승 이런 식으로 표현하듯이 말이다. 그래서 IEEE754는 가수부는 가장 큰 자리수의 비트값이 무조건 1인 것만 인정하게 하고 그 1은 명시적인 표기를 생략했다. 이를 ‘정규화’된 형태라고 한다.

거두절미하고, 32비트 부동소수점에서 구조체의 각 멤버로부터 부동소수점 값을 다시 얻어 오는 식은 다음과 같으므로 참고하자. 식에서 23과 24는 가수부의 자릿수 23비트와 관계가 있으며, 126은 지수부의 크기 8과 관계가 있다. 2의 8-1승보다 2 더 작은 값이다. 1<<23이 바로 생략된 최대 자리수인 셈이다.
그저 의미를 알 수 없는 블랙박스 같던 부동소수점이 좀더 친근하게 와 닿을 것이다.

pow(2.0, (double)( (int)a.expo-24-126))* ((1<<23)|a.mantissa) * (a.sign ? -1:1)

IEEE754에는 이외에도 무한대를 표현하거나 불능을 뜻하는 규격이 있다. 정수를 0으로 나누면 CPU 차원에서 바로 exception이 일어나지만 부동소수점은 에러는 안 나고 저런 특수한 숫자가 돌아온다.
또한 정규화를 해서 자리수를 강제로 늘리는 바람에 표현할 수 없게 된 일부 극히 작은 수를 별도로 표현하기 위해 내부적으로 심지어 ‘고정소수점’ 방식을 임시로 사용하는 규정조차 갖추고 있다. 마치 퀵 정렬이 작은 구간에서는 삽입 정렬을 사용하기도 하는 것처럼 말이다. 이에 대한 더 이상의 자세한 설명은 생략하겠다.

참고로,
1. IEEE754 부동소수점에는 구조적인 한계로 인해 0이 +0과 -0 이렇게 두 개 존재한다.
2. 역사상 최초의 전자식 컴퓨터로 일컬어지는(논란의 여지는 좀 있지만) 에니악은 10진법을 사용했다! 전자 신호 4비트(=16)로 10진법 숫자 하나를 표현했을 것이고 매우 비효율적으로 동작했을 것이다. 하지만 그 당시엔 이 기계로 탄도 계산도 하고 풍동 실험도 하고 심지어 일기 예보도 했었다.

Posted by 사무엘

2010/04/22 22:53 2010/04/22 22:53
, ,
Response
No Trackback , 7 Comments
RSS :
http://moogi.new21.org/tc/rss/response/251


블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2020/09   »
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      

Site Stats

Total hits:
1443465
Today:
283
Yesterday:
512