1980년 인텔이 내놓은 8087은 IBM PC를 비롯한 당시 시스템에서 부동소수점 연산을 획기적으로 빠르게 만든 코프로세서다. 탄젠트, 제곱근, 로그 같은 고급 연산까지 지원했지만, 성능 향상의 체감은 기본 산술 연산에서 가장 먼저 드러났다. 8086 프로세서가 소프트웨어로 처리하던 덧셈을 8087은 거의 100배 빠르게 수행했고, 복잡한 연산에서는 그 격차가 더 벌어졌다. 이번 분석은 실제 칩의 뚜껑을 끌로 뜯어내고 현미경으로 다이(die)를 촬영해, 덧셈과 뺄셈이 마이크로코드와 회로 수준에서 어떻게 구현됐는지를 들여다본 결과다.
칩 안의 연산 엔진
다이 중앙의 큼직한 직사각형 영역이 마이크로코드 ROM으로, 칩 동작을 제어하는 1648개의 마이크로명령이 담겨 있다. 칩의 아래쪽 절반은 80비트 값을 실제로 계산하는 데이터패스다. 그 핵심은 중앙의 정수 가산기로, 한쪽 인자는 B 레지스터에서, 다른 인자는 임시 레지스터나 스택 레지스터 같은 구성요소에서 받아 결과를 '합(sum) 레지스터'에 넣는다. 이 가산기는 64비트를 넘는 69비트 폭을 가지는데, 반올림용 비트 3개와 오버플로·곱셈을 위한 상위 비트가 추가됐기 때문이다. 바로 옆의 시프터는 값을 좌우로 임의의 자릿수만큼 한 번에 밀 수 있어 상당한 면적을 차지한다. 부동소수점 연산이 값을 정렬할 때 시프터를 집중적으로 쓰기 때문이며, 왼쪽의 0 비트 개수를 세는 선행 0 회로와 지수의 오버플로·언더플로를 감지하는 지수 변환기가 함께 성능을 떠받친다.
부동소수점 수는 대개 부호 × 가수(significand) × 2의 지수승으로 표현된다. 가수는 1.bbb… 형태로, 맨 앞 1 다음에 이진 소수점이 오고 나머지 비트가 음의 거듭제곱 자리를 채운다. 예컨대 이진수 1.1은 1 + 1/2 즉 10진수 1.5이고, 1.0011은 1.1875다. 지수는 가수의 좁은 범위를 넘어서게 해주어, 32비트 단정도 수조차 대략 10의 -36승에서 10의 38승에 이르는 범위를 다룬다.
정렬과 정규화라는 관문
덧셈과 뺄셈이 정수 연산보다 번거로운 이유는 이진 소수점을 먼저 맞춰야 하기 때문이다. 13.5 + 3.5를 예로 들면, 두 인자 모두 맨 앞 비트가 1인 정규화 상태인데 지수 차이가 2이므로 둘째 인자를 오른쪽으로 두 자리 민 뒤 정수 덧셈을 한다. 이때 왼쪽에 '여분의' 비트가 생기는 오버플로가 발생하면 결과를 다시 오른쪽으로 한 자리 밀고 지수를 1 올려 정규화한다. 뺄셈은 합병증이 더 많다. 8 - 7.5처럼 지수를 맞춰 뺀 뒤 맨 앞이 1이 되도록 왼쪽으로 밀어야 하는데, 이 경우 4비트를 밀고 지수를 4 내려야 할 만큼 큰 시프트가 필요할 수 있다. 결과가 음수로 나오면 가수를 양수로 바꾸고 부호를 뒤집으며, 아예 0이 되면 정규화가 불가능하므로 상황에 맞게 +0이나 -0을 반환한다.
8087은 내부적으로 모든 수를 80비트 '임시 실수(temporary real)' 형식으로 저장한다. 부호 1비트, 15비트 지수, 64비트 가수 구성으로, 지수만으로도 10의 4932승을 넘는 값을 다룬다. 지수에는 16383의 바이어스를 더해 항상 양수로 저장하므로 지수 1은 0x4000, -16은 0x3fef로 기록된다. 여덟 개의 내부 레지스터는 스택으로 구성되며 각 값에는 영(zero)·유효(valid)·특수(special)·비어 있음(empty)이라는 태그가 붙는다. 무한대·NaN·비정규화 값은 특수로 분류되고, 비어 있음 태그는 스택 언더플로와 오버플로를 감지하는 데 쓰인다. 내부 임시 레지스터 중 tmpA·tmpB는 80비트에 태그 2비트를 갖지만, tmpC는 부호·지수·태그가 없는 대신 68비트 가수에 반올림용 guard·round·sticky 비트를 더해 둔 점이 다르다.
마이크로코드가 그려내는 분기
덧셈·뺄셈·곱셈·나눗셈은 모두 주소 #0001의 동일한 마이크로코드에서 출발해 두 인자를 tmpA·tmpB로 옮긴다. 그다음 연산 종류별 서브루틴으로 분기하는데, 흥미롭게도 분기 테이블의 타깃 0은 곱셈·나눗셈·덧셈뺄셈용 세 개의 주소를 갖도록 ROM에 특수 회로로 구현돼 있다. 설계 단계에서나 가능한 일종의 꼼수 최적화인 셈이다. 덧셈·뺄셈 서브루틴은 #0232에서 시작해 0+0 같은 영 사례를 먼저 처리하고, 두 지수를 빼서 더 작은 쪽이 앞에 오면 인자를 교환한다. 이 교환이 부호를 바꿀 경우 #0244에서 '부호 래치'를 토글해 결과 부호를 추적한다.
실질적인 정렬은 #0247에서 CREATE_DENORM 서브루틴이 맡아 tmpB를 밀어 소수점을 맞춘다. 지수 변화는 두 곳에서 관리되는데, 1비트 단위 시프트는 2비트짜리 expAdj 레지스터(감산 여유를 위해 1로 초기화)가, 더 큰 변화는 #0249에서 0으로 초기화한 tmpB의 지수 필드가 흡수한다. 그리고 #0250에서 핵심 결정이 내려진다. 겉보기 연산이 덧셈이든 뺄셈이든 실제 저수준 연산은 부호 조합에 따라 무부호 덧셈일 수도, 무부호 뺄셈일 수도 있기 때문이다. 8087은 tmpA 부호와 tmpB 부호, 그리고 연산 비트를 배타적 논리합(XOR)으로 묶어 1이면 무부호 뺄셈, 0이면 무부호 덧셈으로 판정하는 전용 회로를 두어 이를 조건 분기로 간단히 검사한다.
이 구조가 한국 실무자에게 주는 시사점은 분명하다. 오늘날 우리가 당연하게 쓰는 부동소수점 '한 줄'의 연산 뒤에는 정렬, 부호 판정, 정규화, 특수값 처리라는 여러 단계가 숨어 있고, 8087은 이를 전용 시프터와 폭넓은 가산기, 그리고 꼼꼼한 마이크로코드 분기로 하드웨어화해 100배의 속도를 확보했다. 다만 이 글이 다루는 범위는 기본 산술에 한정되며, 반올림 모드의 세부 동작이나 비정규화 수의 완전한 처리, 예외 플래그 같은 영역까지 설명하지는 않는다. 또한 여기서 재구성한 흐름은 실제 다이를 뜯어 관찰한 역공학의 결과라는 점에서, 공식 규격 문서를 대체하기보다 그 이면의 설계 의도를 읽어내는 참고 자료로 보는 편이 적절하다.