
1980년 인텔은 IBM PC와 다른 시스템에서 부동소수점 연산을 크게 빠르게 하는 칩 8087을 출시했다. 이 글은 8087의 마이크로코드와 회로를 살펴 덧셈·뺄셈이 구현된 방식을 설명한다. 인텔 문서에 따르면 8087은 8086에서 소프트웨어로 처리하는 것보다 덧셈을 거의 100배 빠르게 수행했으며, 명령에 따라 속도 향상 폭은 달랐다.인텔 문서와 성능 비교
8087은 탄젠트, 제곱근, 로그 같은 연산도 지원하지만, 여기서는 기본 산술 연산인 덧셈과 뺄셈에 초점을 맞춘다. 칩 뚜껑을 끌로 벗기고 현미경으로 다이 이미지를 얻어 회로를 살폈다. 다이 중앙의 큰 직사각형은 칩을 제어하는 마이크로 명령 1,648개를 담은 마이크로코드 ROM이다. 아래쪽 절반은 80비트 값으로 부동소수점 계산을 수행하는 데이터 경로다.데이터 경로 이미지
덧셈과 뺄셈의 중심에는 정수 가산기가 있다. 가산기는 B 레지스터와 임시 레지스터나 스택 레지스터 등에서 받은 값을 더하고, 결과를 합계 레지스터에 저장한다. 시프터는 값을 임의의 비트 수만큼 빠르게 왼쪽이나 오른쪽으로 이동하며, 부동소수점 연산에서 두 값의 자릿수를 맞추는 데 쓰인다. 선행 0 회로는 값의 왼쪽에서 0 비트나 0 바이트가 몇 개 이어지는지 세어 제거할 양을 계산한다. 지수 변환기는 지수를 처리하고 오버플로와 언더플로를 감지한다. 이 전용 회로들이 8087의 빠른 연산을 뒷받침했다.
부동소수점 덧셈과 뺄셈
정수만으로는 1.23 같은 소수와 매우 큰 범위의 수를 표현하기 어렵다. 컴퓨터는 부호 × 가수 × 2의 지수승 형태로 부동소수점 수를 표현해 이 문제를 해결한다. 가수는 1.bbb…처럼 맨 앞에 1이 오고 이진점 뒤에 나머지 비트가 놓이는 이진수다. 이진수 1.1은 1+1/2, 즉 십진수 1.5이고, 1.0011은 1+1/8+1/16, 즉 1.1875다. 지수를 사용하면 가수가 표현할 수 있는 범위를 크게 넓힌다. 예를 들어 이진수 1.1에 2의 8승을 곱하면 384다. 32비트 단정도 부동소수점 수도 10의 -36승보다 작은 값부터 10의 38승보다 큰 값까지 표현할 수 있다.
부동소수점 덧셈과 뺄셈은 정수 연산보다 복잡하다. 연산에 앞서 두 수의 이진점이 맞도록 지수를 같게 해야 한다. 연산 뒤에는 가수가 1로 시작하도록 정규화하고 결과의 지수를 조정한다.
예를 들어 13.5+3.5를 계산할 때 두 수는 각각 지수와 가수로 표현된다. 두 가수의 이진점을 맞추기 위해 지수가 작은 두 번째 값을 오른쪽으로 두 자리 이동한다. 정렬된 가수를 정수 덧셈으로 더하면 이 경우 왼쪽에 여분의 비트가 생긴다. 결과를 한 자리 오른쪽으로 이동하고 지수를 1 늘려 정규화한다.덧셈 단계 그림
뺄셈에서는 결과가 양수·음수·0이 될 수 있고, 두 입력보다 훨씬 작은 결과가 나올 수도 있다. 8-7.5의 경우에도 지수를 맞추도록 두 번째 가수를 오른쪽으로 이동한 뒤 정수 뺄셈을 한다. 결과의 맨 앞 비트가 1이 되도록 왼쪽으로 이동해 정규화한다. 이 예에서는 네 비트 이동하고 지수를 4 줄인다.뺄셈 단계 그림
뺄셈 결과가 음수면 가수를 양수로 바꾸고 결과의 부호를 뒤집는다. 결과가 0이면 정규화할 수 없으므로 상황에 맞는 +0 또는 -0을 반환한다.
8087의 데이터 형식과 연산 회로
8087은 여러 자료형을 지원하지만 내부에서는 80비트 형식인 ‘임시 실수(temporary real)’로 값을 저장한다. 이 형식은 부호 1비트, 지수 15비트, 가수 64비트로 구성된다. 15비트 지수는 10의 4932승을 넘는 값까지 나타낼 수 있고, 64비트 가수는 높은 정밀도를 지원한다. 지수에는 16,383의 바이어스를 더해 저장하므로 저장된 값은 항상 양수다. 실제 지수 1은 0x4000, -16은 0x3fef로 저장된다.임시 실수와 레지스터 형식 그림
8087에는 스택 형태로 구성된 내부 레지스터가 8개 있으며, 프로그래머는 이 레지스터를 사용한다. 각 레지스터는 80비트 부동소수점 수와 태그를 가진다. 태그는 0, 유효, 특수, 비어 있음 중 하나다. 정상적인 부동소수점 값은 유효로, 무한대·NaN(숫자가 아님)·비정규 값은 특수로 표시한다. 비어 있음 태그는 레지스터에 값이 없다는 뜻이며, 빈 레지스터를 읽는 스택 언더플로와 값이 든 레지스터에 쓰는 스택 오버플로를 감지하는 데 쓰인다.
내부 임시 레지스터 tmpA와 tmpB는 각각 80비트이며 태그 비트 두 개도 가진다. tmpC에는 부호·지수·태그가 없다. 가수는 68비트로, 오버플로를 막는 비트 하나와 반올림용 하위 비트 세 개(가드, 라운드, 스티키)가 추가돼 있다.
마이크로코드의 각 16비트 마이크로 명령은 저수준 작업 하나를 수행한다. 데이터 이동 외에도 조건 분기, 서브루틴 호출과 복귀가 있다. 핵심 연산 장치는 69비트 정수 가산기다. 64비트보다 넓은 것은 반올림 비트 세 개와 오버플로·곱셈을 위한 상위 비트가 있기 때문이다. 정수 덧셈은 두 마이크로 명령으로 수행한다. 첫 명령은 첫 번째 피연산자와 올림·반올림 같은 옵션을 지정하며, 뺄셈에서는 옵션으로 두 번째 피연산자를 보수화한다. 두 번째 피연산자는 이름이 비슷하지만 tmpB와는 관계없는 B 레지스터에서 온다. 결과는 합계 레지스터에 놓이고, 별도의 마이크로 명령이 이를 목적지로 옮긴다.
부동소수점 연산은 68비트 값을 임의의 거리만큼 왼쪽이나 오른쪽으로 이동하는 시프터도 폭넓게 사용한다.8087 가산기 분석와 8087 수학 보조 프로세서 다이 분석에서 관련 회로를 다뤘다. 시프트도 두 마이크로 명령으로 이뤄진다. 첫 명령이 값을 시프터로 보내고 방향과 이동량을 정하며, 두 번째 명령이 결과를 목적지로 옮긴다.
덧셈·뺄셈 마이크로코드의 흐름
8087은 거의 모든 연산을 마이크로코드로 구현한다. 덧셈·뺄셈·곱셈·나눗셈은 모두 10진 주소 #0001에서 같은 마이크로코드를 실행해 두 피연산자를 tmpA와 tmpB에 넣는다. 보통 스택 레지스터에서 값을 가져오며, 메모리에서 받을 수도 있지만 여기서는 다루지 않는다.
이후 연산 종류에 맞는 마이크로 서브루틴을 호출한다. 마이크로코드 엔진에는 분기나 서브루틴 호출의 목적지 주소를 담은 작은 ROM이 있다. 목적지 0은 예외로 곱셈·나눗셈·덧셈과 뺄셈에 각각 다른 주소를 제공한다. ROM의 전용 회로가 이를 구현한다.
덧셈·뺄셈 서브루틴은 #0232에서 시작한다. 먼저 0+0이나 0-0을 처리한 뒤 두 피연산자의 지수를 뺀다. 첫 번째 피연산자의 지수가 더 작으면 더 큰 지수를 가진 값이 앞에 오도록 두 피연산자를 바꾼다. 부호 래치가 결과의 부호를 기억하며, 순서를 바꿔 부호가 달라질 때는 #0244에서 래치를 토글한다. 덧셈·뺄셈 마이크로코드 흐름도
#0232 jmp #0236 if not tmpA:tag ZERO Check for 0+0
#0233 jmp #0236 if not tmpB:tag ZERO
#0234 return if not unsigned sub 0+0 same signs
#0235 jmp #0263 handle 0+0 different signs
#0236 tmpA sign -> sign latch nonzero: tmpA sign to sign latch
#0237 tmpB:exp -> Breg
#0238 adder: tmpA:exp - Breg cin=1 compare exponents
#0239 jmp #0245 if adder pos
#0240 sumreg:frac -> Breg swap if tmpB's exponent larger
#0241 adder: zero - Breg cin=1
#0242 xchg tmp
#0243 jmp #0245 if not unsigned sub
#0244 !sign latch -> sign latch toggle sign if swapping matters
#0245 xchg tmp continue
#0246 shift zero R 0 bytes, 0 bits zero
#0247 call CREATE_DENORM shift tmpB to match exponents
#0248 xchg tmp
#0249 zero -> tmpB:exp store exp adjustment in tmpB:exp
#0250 jmp #0269 if not unsigned sub base addition?
#0251 adder: tmpA:frac - Breg cin=1, roundmode tmp A - tmpB
#0252 jmp #0256 if adder pos
#0253 sumreg:frac,rnd -> Breg/rnd negate frac
#0254 adder: zero - Breg cin=1, roundmode
#0255 !sign latch -> sign latch toggle sign
#0256 jmp #0275 if Sum register is zero jump if result zero
#0257 shift R -> tmpB:frac orig tmpB, from CREATE_DENORM
#0258 jmp #0282 if tmpA top bit tmpA not denorm?
#0259 jmp #0282 if tmpB:frac[63] tmpB not denorm?
#0260 jmp #0289 skip normalization if both denorms
#0261 tmpA:exp -> expConv handle zero
#0262 jmp #0264 if not tmpA top bit
#0263 zero -> tmpA handle zero cases
#0264 0 -> tmpA sign +0
#0265 zero -> tmpA:frac
#0266 return if not round chop
#0267 1 -> tmpA sign return -0
#0268 return
#0269 adder: tmpA:frac + Breg cin=0, roundmode base addition: tmpA + tmpB
#0270 jmp #0289 if not sumreg[64] check for overflow
#0271 shift 1 sumreg:sign,frac,rnd R shift right 1 bit to normalize
#0272 shift R -> sumreg:frac,sign,rnd
#0273 expAdj++ increment exp adjustment
#0274 jmp #0289 wrap up addition
#0275 shift sumreg:frac,rnd L 0 bytes, 3 bits zero? shift to get rounding bits
#0276 shift L -> sumreg:frac
#0277 jmp #0261 if Sum register is zero jump if no rounding bits
#0278 zero -> sumreg:frac clear result
#0279 jmp #0289 if not tmpA top bit denorm?
#0280 expAdj-- if not denorm, adjust exp down
#0281 1 -> sumreg:frac,rnd set lowest bit in result
#0282 lzcount -> shiftcountH leading zero adjust: handle bytes
#0283 shift sumreg:frac,rnd L count byte sticky
#0284 shift L -> sumreg:frac,rnd
#0285 lzcount -> shiftcountL handle leading zero bits
#0286 shift sumreg:frac,rnd L count bit sticky
#0287 shift L -> sumreg:frac,rnd
#0288 shiftcount -> tmpB:exp remember the bit shift amount
#0289 sign latch -> tmpA sign store sign
#0290 call ADJUST_PRECISION if not FBLD/FBSTP adjust precision if necessary
#0291 sumreg:frac -> tmpA:frac store adjusted frac part
#0292 tmpB:exp -> Breg get exp
#0293 adder: tmpA:exp - Breg cin=0 adjust by Breg
#0294 expAdj -> Breg
#0295 adder: sumreg:frac + Breg cin=0 adjust by expAdj
#0296 sumreg:frac -> expConv exp in expConv to check for overflow, etc
#0297 sumreg:frac -> tmpA:exp store exp in tmpA
#0298 return done
#0247에서 CREATE_DENORM 서브루틴이 tmpB를 이동해 두 값의 이진점을 맞춘다. 이동량은 앞서 지수 차를 계산해 합계 레지스터에 넣어 둔다. 차가 음수였다면 보수화했으므로 전달되는 값은 양수다.
결과의 지수는 보통 더 큰 값의 지수와 같지만 항상 그런 것은 아니다. 한 비트 이동에 따른 조정은 2비트 레지스터 expAdj에 기록한다. 이 레지스터는 감소 여지를 두기 위해 1로 초기화한다. 더 큰 지수 변화는 tmpB의 지수 필드에 기록하며, 마이크로코드는 #0249에서 이 값을 0으로 초기화한다.
#0250에서는 낮은 수준의 연산이 부호 없는 덧셈인지 뺄셈인지 결정한다. 부호에 따라 명령의 이름과 가수의 실제 연산은 다를 수 있다. 2+3, -2-3, -2+(-3)처럼 크기를 더하는 경우는 부호 없는 덧셈이고, -2+3, 2-3, 3-2처럼 크기를 빼는 경우는 부호 없는 뺄셈이다. 따라서 덧셈 명령도 부호 없는 뺄셈으로 이어질 수 있고, 뺄셈 명령도 부호 없는 덧셈으로 이어질 수 있다.
8087에는 두 부호와 연산 종류를 바탕으로 이를 판별하는 전용 회로와 조건 분기가 있다. 음수를 1, 양수를 0으로 두고 뺄셈을 1, 덧셈을 0으로 표시하면 tmpAsign⊕tmpBsign⊕op의 결과가 1일 때 부호 없는 뺄셈, 0일 때 부호 없는 덧셈이다. 두 부호가 같은지 다른지를 검사하는 조건 분기도 있다.
부호 없는 덧셈은 #0269에서 시작한다. 작은 수인 tmpB를 지수가 같아질 때까지 오른쪽으로 이동하고 두 크기를 더한다. 최상위 비트에서 올림이 생기면 #0271에서 결과를 오른쪽으로 한 비트 옮기고, expAdj를 늘려 지수 조정을 기록한다. #0289에서 부호 래치의 값을 결과 부호에 반영한다. 8087은 80비트 임시 실수, 64비트 실수, 32비트 실수 세 가지 크기를 지원한다. 작은 형식에서는 ADJUST_PRECISION이 결과를 해당 정밀도에 맞춘다. 반올림으로 값이 커지면 지수도 조정해야 할 수 있으며, 이는 expAdj에 기록된다.
최종 지수는 초기 지수에서 tmpB 지수의 조정량을 빼고, expAdj의 조정량을 더한 뒤 초기값 1을 보정하기 위해 1을 뺀다. 지수는 결과에 저장하는 동시에 오버플로나 언더플로를 검사하는 지수 변환기에도 전달한다. 공통 마이크로코드는 범위를 벗어난 결과가 있으면 NONNORMAL_RESULT를 호출한다. 이 서브루틴은 설정에 따라 인터럽트를 일으키거나 무한대 또는 0을 반환할 수 있다.비정규 결과 처리
결과는 적절한 스택 레지스터에 기록된다. 명령이 마지막에 스택 팝을 지정했다면 스택 맨 위를 비어 있음으로 표시하고 스택 포인터를 갱신한다. 마지막으로 Run Next Instruction 마이크로 명령이 현재 명령의 마이크로코드 실행을 끝낸다.
부호 없는 뺄셈은 #0251에서 시작한다. 여기에도 -2+3처럼 덧셈 명령이 포함될 수 있다. 가산기는 tmpA-tmpB를 계산하며, 두 번째 값을 보수화해 뺄셈을 수행한다. 결과는 양수나 음수일 수 있다. 이어서 부호를 정하고 정밀도와 최종 지수를 조정하는 과정은 덧셈과 같다.
뺄셈 결과가 0이면 #0275에서 반올림 비트 세 개를 검사한다. 반올림 비트가 설정돼 있으면 결과의 최하위 비트를 설정하고, 이를 상쇄하도록 지수 조정을 1 줄인다. 비정규 수에서는 반올림 비트를 올릴 여분의 자리가 없으므로 결과를 지운다. 결과가 실제로 0이면 #0261의 별도 경로에서 +0과 -0 중 어느 것을 반환할지 결정한다. 0-0도 이 경로에 들어와 +0과 -0의 조합을 처리한다. 예를 들어 ⁺0+⁺0은 ⁺0이고 ⁻0-⁺0은 ⁻0이다. ⁺0+⁻0이나 ⁻0-⁻0처럼 0끼리 상쇄되는 경우에는 내림 반올림 모드에서 ⁻0을 반환하고, 나머지 반올림 모드에서는 ⁺0을 반환한다.
선행 0 회로는 결과를 얼마나 이동할지 계산한다. 회로를 단순하게 만들기 위해 이동량은 두 번에 나눠 구한다. 먼저 선행 0 바이트 수만큼 왼쪽으로 이동한 다음, 남은 선행 0 비트를 세어 0~7비트만큼 더 이동한다. 전체 이동량은 tmpB의 지수에 저장한다. 부호 없는 뺄셈 경로는 #0289에서 덧셈 경로와 합쳐져 정밀도 조정과 지수 계산을 거쳐 결과를 저장한다.
무한대와 특수 값 처리
8087은 무한대, 0, NaN, 비정규 수와 그 밖의 특수 값을 여러 경로로 처리한다. 설계자들은 이를 수학적으로 타당하게 다루려 했지만, 그 결과 마이크로코드는 복잡해졌다.윌리엄 카한의 부동소수점 설계 논의
덧셈·뺄셈·곱셈·나눗셈이 공유하는 공통 마이크로코드는 인수를 읽고 결과를 저장하며 여러 특수 사례를 처리한다. 조건 분기가 연이어 18개 나오는 복잡한 흐름이다. 아래 코드는 인수 처리와 특수 값 판별의 주요 경로를 보여준다.공통 마이크로코드 흐름도
#0001 call MEM_OP if mem op maybe get args from memory
#0002 jmp #0004 if mem op
#0003 st(i) -> tmpB get from stack
#0004 jmp #0018 if DE (denormalized) interrupt quit if denormalized interrupt
#0005 st(0) -> tmpA top of stack to tmpA
#0006 jmp #0008 if not reverse bit no jump if FSUBR, FMUL, FDIVR?
#0007 xchg tmp reverse arguments
#0008 jmp #0019 if tmp anomalous handle anomalous values
#0009 call INSTR_SWITCH switch on instruction type: J1(mul), J2(add/sub), J3(div)
#0010 call NONNORMAL_RESULT if not exp normal Back, finish up instruction
#0011 jmp #0014 if dst st(i) test d destination bit
#0012 tmpA -> st(0) destination st(0)
#0013 RNI
#0014 tmpA -> st(i) exit: destination st(i)
#0015 jmp #0018 if not pop at end RNI if not pop
#0016 empty -> SP(0) tag mark top of stack empty
#0017 stackPtr++ pop
#0018 RNI
#0019 call SPECIAL_TMPS handle special values
#0020 jmp #0023 if not flag
#0021 jmp #0011 if expConv[0] jump to finish
#0022 jmp #0009 switch on instruction type
#0023 cond3b -> sign latch more conditions
#0024 jmp #0039 if expConv[0]
#0025 jmp #0032 if not tmpB:tag ZERO
#0026 jmp #0035 if tmpA:tag ZERO
#0027 except:zero div
#0028 NaN -> tmpA NaN, finish
#0029 high bit -> tmpA:frac
#0030 sign latch -> tmpA sign
#0031 jmp #0037 finish up
#0032 jmp #0035 if not tmpA:tag ZERO divide by denorm/0
#0033 tmpB:frac -> sumreg:frac divide by 0
#0034 jmp #0055 if not Sum register is zero
#0035 except:invalid
#0036 NaN -> tmpA
#0037 jmp #0018 if intr jump to RNI
#0038 jmp #0011 finish
#0039 jmp #0051 if not tmpA:tag SPECIAL more conditions
#0040 jmp #0046 if not tmpB:tag SPECIAL
#0041 jmp #0028 if FMUL
#0042 jmp #0035 if FDIV FDIVR
#0043 jmp #0035 if not affine ∞ ∞×∞: test if affine
#0044 jmp #0035 if unsigned sub test if ∞ add or subtract
#0045 jmp #0038
#0046 jmp #0049 if not FMUL
#0047 jmp #0035 if tmpB:tag ZERO
#0048 jmp #0028
#0049 jmp #0028 if FDIV FDIVR
#0050 jmp #0038
#0051 jmp #0054 if not FMUL
#0052 jmp #0035 if tmpA:tag ZERO divide by 0?
#0053 jmp #0028
#0054 jmp #0057 if not FDIV FDIVR
#0055 zero -> tmpA 0÷denorm or n÷∞
#0056 jmp #0030 do something and finish up
#0057 xchg tmp
#0058 jmp #0061 if sign mismatch
#0059 jmp #0062 if unsigned sub test if adding or subtracting
#0060 jmp #0038 finish
#0061 jmp #0038 if unsigned sub finish
#0062 tmpA sign -> sign latch complement sign
#0063 !sign latch -> tmpA sign
#0064 jmp #0038 finish
#0008에는 임시 레지스터가 비어 있거나 특수 값으로 표시됐는지 검사하는 조건이 있다. 해당하면 SPECIAL_TMPS 서브루틴이 두 임시 레지스터를 살펴 특수 처리가 필요한지 나타내는 플래그 두 개를 설정한다. 두 플래그가 모두 설정되면 인수 중 하나가 비어 있다는 뜻이며, 보통 스택 언더플로를 나타내므로 마이크로코드는 NaN을 반환한다. 두 번째 플래그가 꺼져 있다면 인수 중 하나는 0에 매우 가까운 특수 값인 비정규 수이며 연산을 계속한다.
나머지 경로는 부호, 연산 종류, 0이나 무한대 여부에 따라 갈린다. 다음 여덟 경우에는 NaN을 반환한다. n÷denorm, 0÷0, 0÷pseudo-zero, ∞×0, ∞÷∞, 0×∞, ∞ op ∞(projective), ∞-∞(affine). 0×∞처럼 의미 있는 결과가 없는 경우도 있지만, 마지막 두 경우는 8087의 무한대 처리 방식과 관련이 있다.
8087은 제어 레지스터의 비트로 선택하는 무한대 모드 두 가지를 지원한다. ‘아핀 폐포(affine closure)’에서는 무한대에 부호가 있어 +∞와 -∞가 서로 다른 값이다. 권장 모드는 무한대에 부호가 없는 ‘사영 폐포(projective closure)’다. 아핀 모드에서는 ∞+∞가 유효해 양의 무한대가 되지만 ∞-∞는 결과를 0으로 정할 근거가 없으므로 NaN이다. 사영 모드에서는 무한대가 양의 무한대이자 음의 무한대이므로 무한대 연산은 모두 의미가 없다.
무한대를 반환하는 경로는 n÷0, ∞×n, ∞×∞, n×∞, ∞÷x, x±∞, ∞±x, ∞+∞(아핀 모드) 등 여덟 가지다. 첫 번째 경우는 0으로 나누기 예외가 마스크된 상황을 전제로 한다. 앞의 다섯 경우에는 두 인수의 부호를 조합해 무한대의 부호를 만든다. 나머지 세 경우에는 무한대 인수의 부호만 사용하고 다른 인수의 부호는 무시한다. 즉 곱셈과 나눗셈은 무한대의 부호를 바꿀 수 있지만 덧셈과 뺄셈은 바꾸지 않는다. 0을 만드는 특수 경로는 0÷denorm과 n÷∞이며, 두 인수의 부호를 이용해 0의 부호를 정한다.
선행 0 회로
뺄셈에서는 두 값의 자릿수가 상쇄돼 결과 앞에 0이 이어질 수 있다. 이를 없애려면 결과를 이동해야 한다. 선행 0 회로는 첫 1 앞에 있는 0의 수를 세어 이동량으로 바꾼다. 이는 첫 1의 위치를 인코딩하는 우선순위 인코더와 비슷하다.
64비트 전체를 한꺼번에 처리하는 회로는 복잡하다. 입력 비트마다 출력 비트가 달라지기 때문이다. 8087은 대신 바이트 단위로 선행 0을 없앤 뒤 남은 0~7개 비트를 처리하는 두 단계 방식을 쓴다. 각 단계에는 8입력 선행 0 검출 회로가 필요하다. 이 방식은 64비트 검출 회로보다 느리지만 회로를 단순하게 만든다.선행 0 검출 회로
회로는 선행 0이 0~7개인 경우를 3비트 이진수로 출력한다. 위쪽 NOR 게이트는 1·3·5·7개의 선행 0일 때 설정되는 최하위 비트를 만든다. 아래쪽 NOR 게이트는 4·5·6·7개일 때 상위 비트를 만든다. 가운데 비트에는 4개나 5개의 선행 0인지 판단하는 논리가 한 단계 더 들어간다. 설명을 단순화하면 예외가 있지만, 가능한 256가지 입력을 모두 열거해 출력이 맞는지 확인할 수 있다. 선행 0 바이트를 세는 회로도 비슷하며, 각 8비트 그룹에 OR 게이트를 적용해 그 바이트에 1이 있는지 확인한다. 다이에서는 선행 0 회로가 합계 레지스터 왼쪽에 있어 레지스터의 각 비트를 살핀다.
설계가 남긴 흔적
덧셈과 뺄셈 뒤에는 양수·음수 0, 양수·음수 무한대, NaN을 다루는 규칙과 전용 실리콘 회로가 있다. 8087에서 이어진 이 처리 방식은 오늘날 사용하는 IEEE-754 부동소수점 표준에도 남았다. 다만 8087은 일부 특수 사례를 지나치게 많이 지원했다. 사영 무한대, 비정규(normal이 아닌 unnormal) 값, pseudo-zero 같은 기능은 표준에서 노력만큼의 가치가 없다고 여겨져 빠졌다.
8087 마이크로코드 역공학은 계속 진행 중이다. 작업에는 Opcode Collective 구성원, 특히 ROM 이미지를 마이크로코드 데이터로 변환하고 내용을 분석한 Smartest Blob과 Gloriouscow가 참여했다. 8087 저장소에서 관련 자료를 볼 수 있다. 이 글의 작성에는 AI를 사용하지 않았으며, Opcode Collective는 ROM 셀을 분류해 마이크로코드를 추출하는 데 머신러닝 알고리즘을 사용했다. AI 사용 관련 설명
참고 자료
8087 내부는 부동소수점 값을 부호 1비트, 지수 15비트, 가수 64비트로 저장하지만, 반올림을 위해 가드·라운드·스티키라는 하위 비트 세 개를 더 사용한다. 데이터 경로 일부에는 부호와 오버플로를 위한 비트도 있다. 시프터는 68비트, 가산기는 69비트다. 설명에서는 대체로 이런 추가 비트를 제외하고 데이터 경로를 64비트로 다룬다.
부동소수점은 일부 문제를 해결하지만 새로운 문제도 만든다. 예를 들어 0.01은 이진 부동소수점으로 정확하게 표현할 수 없다. 십진수로 1/3을 정확히 나타낼 수 없는 것처럼 이진수에서 무한히 반복되는 소수이기 때문이다. 센트 단위 금액을 부동소수점으로 표현하면 문제가 생길 수 있다. 자세한 내용은 줄리아 에번스의 만화와 모든 컴퓨터 과학자가 부동소수점 산술에 관해 알아야 할 것을 참고할 수 있다.
임시 실수 형식과 외부 형식의 차이 중 하나는 임시 실수가 맨 앞의 1을 실제로 저장한다는 점이다. 외부 형식에서는 이 1을 암묵적으로 처리해 저장하지 않는다. 덕분에 정밀도는 1비트 늘지만 연산은 더 복잡해진다.
부동소수점 전문가 윌리엄 카한은 설계 결정의 배경을 설명하면서 x**0 = 1을 0, ∞, NaN을 포함한 모든 x에 적용하자는 의견이 늘고 있다고 썼다. NaN이 NaN과 같지 않은 이유 등은 카한의 문서에 정리돼 있다.
SPECIAL_TMPS 서브루틴은 상태를 플래그 플립플롭과 지수 변환기 레지스터 expConv에 기록한다. 빈 값은 예외와 NaN을, 더 큰 쪽의 NaN도 예외와 NaN을, 무한대는 무한대를, 비정규 수는 비정규 결과를 나타낸다. ‘0으로 나누기 비슷한 경우’에는 나눗셈이 0이나 비정규 수에 가까운 비정규 값으로 나뉘는지를 표시하며 상태는 바뀌지 않는다.
정상 부동소수점 수는 가수의 맨 앞 비트가 1이어야 한다. 하지만 가장 작은 정상 수와 0 사이에는 큰 간격이 생긴다. 비정규 수는 맨 앞 비트가 0이어도 허용해 0에 가까운 값을 더 많이 표현한다. 십진수로 1.003, 1.002, 1.001, 1.000 다음에 맨 앞 숫자를 계속 1로 유지해야 한다면 다음 수가 0이 되는 것과 비슷하다. 비정규 수를 허용하면 0.999, 0.998부터 0.001까지 이어갈 수 있다. 비정규 수의 지수는 가능한 최솟값이다. 비정규 예외가 마스크되면 비정규 수를 비정규가 아닌 값(unnormal)으로 바꾸고 실행을 이어갈 수 있다. 8087은 부호 있는 실제 0인 +0과 -0도 지원하며, 실제 0은 최소 지수로 저장한다. 더 큰 지수를 가진 0인 pseudo-zero도 지원한다. 이는 매우 작은 두 비정규 값의 곱셈에서 생길 수 있다. 인텔은 80387에서 unnormal과 pseudo-zero를 없앴다. SPECIAL_TMPS 마이크로코드의 자세한 분석은 이전 글에 있다.
저자의 Bluesky 계정, Mastodon 계정, RSS 피드에서 후속 소식을 볼 수 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요