Intel 8087의 마이크로코드와 회로가 부동소수점 덧셈과 뺄셈, 특수값 처리를 어떻게 구현하는지 살펴봅니다.
1980년에 Intel은 IBM PC와 기타 시스템에서 부동소수점 연산을 훨씬 빠르게 만들어 준 8087 칩을 출시했습니다. 8087은 탄젠트, 제곱근, 로그 같은 고급 연산을 지원하지만, 이 글에서는 기본 산술 연산인 덧셈과 뺄셈에 초점을 맞춥니다. 8087은 8086 프로세서보다 눈부신 속도 향상을 제공하여 덧셈을 거의 100배 빠르게 했고, 복잡한 연산은 그보다도 더 크게 가속했습니다.1 이 글에서는 8087의 마이크로코드와 회로를 조사하여 무한대 및 기타 특수값에 관한 수많은 특수 사례를 포함해 부동소수점 덧셈과 뺄셈이 정확히 어떻게 구현되는지 설명합니다.
8087의 회로를 살펴보기 위해 끌로 칩의 뚜껑을 열고 현미경으로 아래의 고해상도 이미지를 만들었습니다. 마이크로코드 ROM은 다이 중앙의 큰 직사각형 영역으로, 칩을 제어하는 1648개의 마이크로명령어를 담고 있습니다. 칩의 아래쪽 절반(빨간 상자)은 데이터패스이며, 80비트 값으로 부동소수점 계산을 수행하는 회로입니다.2
덧셈과 뺄셈에 사용되는 기능 블록을 보여 주는 8087 데이터패스 확대 이미지. 더 큰 버전을 보려면 이 이미지(또는 다른 이미지)를 클릭하세요.
데이터패스를 확대하면 덧셈과 뺄셈에 중요한 기능 블록이 보입니다. 핵심 구성 요소는 중앙에 있는 정수 가산기입니다. 이 가산기는 B 레지스터에서 한 인수를 받고, 임시 레지스터나 스택 레지스터 같은 구성 요소에서 다른 인수를 받습니다. 가산기의 출력은 합 레지스터에 저장됩니다. 시프터는 값의 비트를 왼쪽이나 오른쪽으로 이동할 수 있습니다. 시프터는 임의의 비트 수만큼 값을 빠르게 이동할 수 있도록 하는 회로 때문에 다이에서 많은 공간을 차지합니다. 부동소수점 연산은 값을 정렬하기 위해 시프터를 많이 사용합니다. 선행 0 회로는 값의 왼쪽에 있는 0 비트 또는 바이트 수를 세어 이들을 이동으로 제거할 수 있게 합니다. 지수 변환기는 지수를 처리하여 오버플로와 언더플로를 감지합니다. 이러한 특수 회로가 8087의 높은 성능에 기여했습니다.
대부분의 컴퓨터는 숫자의 기본 표현으로 정수를 사용합니다. 하지만 많은 응용 프로그램은 1.23처럼 소수 부분이 있는 숫자를 사용해야 합니다. 게다가 정수의 범위는 매우 제한적입니다. 32비트 정수는 약 -20억에서 20억까지의 숫자만 표현할 수 있습니다. 이러한 문제를 해결하기 위해,3 컴퓨터는 보통 엄청나게 넓은 범위에서 소수 숫자를 제공하는 부동소수점 숫자를 구현합니다.
대부분의 경우 부동소수점 숫자는 부호 × 가수 × 2 _지수_로 표현됩니다. 가수는 1.bbb… 형태의 이진수입니다. 즉, 선행 1 뒤에 이진 소수점(십진 소수점에 해당)과 나머지 비트가 옵니다.4 이진수에서 각 비트는 2의 거듭제곱을 나타냅니다. 소수 부분, 즉 이진 소수점 뒤의 부분에서는 2의 거듭제곱이 음수입니다. 2-1, 2-2 등이 그렇습니다. 따라서 이진수 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입니다. 더하기 전에 두 번째 인수를 오른쪽으로 두 자리 이동해 지수를 같게 만들어 정렬해야 합니다. 이 경우 지수의 차이는 둘입니다. 그런 다음 정렬된 가수에 정수 덧셈을 사용해 값을 더합니다. 이 경우 값을 더하면 오버플로가 발생하여 왼쪽에 “추가” 비트가 생깁니다. 결과는 오른쪽으로 한 자리 이동하고 지수를 1 증가시켜 정규화해야 합니다.
부동소수점 뺄셈도 비슷하지만 몇 가지 복잡한 점이 더 있습니다. 결과는 양수, 음수 또는 0일 수 있습니다. 결과는 인수보다 자릿수 규모가 훨씬 작을 수도 있습니다. 아래 그림은 기본적인 뺄셈 8-7.5를 보여 줍니다. 덧셈과 마찬가지로 두 번째 인수를 오른쪽으로 이동하여 지수를 맞춥니다. 즉, 이진 소수점이 일렬로 놓입니다. 두 가수는 정수 뺄셈으로 뺍니다. 결과는 최상위 비트가 1이 되도록 왼쪽으로 이동해 정규화합니다. 덧셈과 달리 정규화에는 큰 이동이 필요할 수 있습니다. 이 경우 이동은 네 비트이고, 결과를 얻기 위해 지수는 4 감소합니다.
단순한 부동소수점 뺄셈.
뺄셈의 한 가지 복잡한 점은 결과가 음수가 될 수 있다는 것입니다. 이 경우 가수는 양수로 만들고 결과의 부호를 뒤집습니다. 뺄셈 결과가 0이 될 수도 있습니다. 이 경우 정규화를 수행할 수 없으므로, 대신 적절한 특수값 +0 또는 -0을 반환합니다.
이 절에서는 덧셈과 뺄셈의 마이크로코드 구현에서 중요한 역할을 하는 8087의 몇 가지 기능을 설명하겠습니다.
8087은 다양한 데이터 형식을 지원하지만, 내부적으로는 “임시 실수”라고 부르는 80비트 형식으로 숫자를 저장합니다. 임시 실수 숫자는 부호 비트, 15비트 지수, 64비트 가수를 가집니다. 15비트 지수는 10 4932를 넘는 터무니없이 큰 숫자를 허용합니다. 64비트 가수는 매우 정확한 숫자를 지원합니다. 지수는 여기에 16383의 “바이어스”를 더해 저장합니다. 따라서 실제 지수가 음수여도 저장된 지수는 항상 양수입니다. 예를 들어 지수 1은 0x4000으로 저장되고, 지수 -16은 0x3fef로 저장됩니다.
80비트 임시 실수 형식과 레지스터 형식. 점은 십진 소수점에 해당하는 이진 소수점을 나타냅니다.
8087에는 스택으로 구성된 여덟 개의 내부 레지스터가 있습니다. 프로그래머는 8087을 프로그래밍할 때 이 레지스터를 사용합니다. 각 레지스터는 80비트 부동소수점 숫자를 보관합니다. 성능을 최적화하기 위해 레지스터 스택의 각 값에는 0, 유효, 특수 또는 비어 있음이라는 연관된 “태그” 값도 있습니다. 값 0에는 0 태그가 붙습니다. “정상” 부동소수점 값에는 유효 태그가 붙습니다. 값이 무한대, 숫자가 아님(NaN) 또는 비정규화 값이면 특수 태그가 붙습니다. 마지막으로 비어 있음 태그는 레지스터가 값을 보관하지 않음을 나타냅니다. 이를 통해 스택 언더플로(빈 레지스터 읽기)나 스택 오버플로(비어 있지 않은 레지스터에 저장)를 감지할 수 있습니다.
8087에는 내부적으로 사용하는 임시 레지스터도 있습니다. tmpA와 tmpB는 각각 두 개의 태그 비트를 갖춘 80비트 레지스터입니다. 그러나 tmpC는 다릅니다. 부호, 지수 또는 태그가 없습니다. 또한 가수는 68비트입니다. tmpC에는 오버플로 방지를 위한 추가 비트 하나와 반올림을 위한 추가 하위 비트 세 개, 즉 가드, 라운드, _스티키_가 있습니다.
마이크로코드에서 각 16비트 마이크로명령어는 하나의 저수준 연산을 수행합니다. 많은 연산은 한 레지스터에서 다른 레지스터로 데이터를 옮깁니다. 다른 연산은 조건부 점프, 서브루틴 호출 및 반환을 제공합니다.
8087 처리의 핵심은 제가 여기에서 다룬 69비트 정수 가산기입니다. 가산기에 64비트보다 많은 비트가 있는 이유는 반올림 비트 세 개와 오버플로 및 곱셈을 위한 추가 상위 비트 몇 개가 있기 때문입니다. 정수 덧셈에는 두 개의 마이크로명령어가 필요합니다. 첫 번째 마이크로명령어는 더할 첫 번째 인수를 지정하고 캐리와 반올림 같은 여러 옵션을 선택합니다. 뺄셈의 경우 한 옵션이 두 번째 인수를 보수화합니다. 가산기의 두 번째 인수는 B 레지스터에서 항상 오는데, 혼란스럽게도 이는 tmpB 레지스터와 관련이 없습니다. 덧셈 연산의 결과는 합 레지스터라는 특수 레지스터에 놓입니다. 두 번째 마이크로명령어는 첫 번째 명령어 뒤로 여러 명령어가 지난 후일 수도 있으며, 합 레지스터에서 목적지로 결과를 옮깁니다.
부동소수점은 임의의 위치 수만큼 68비트 값을 왼쪽 또는 오른쪽으로 이동하는 비트 시프터도 광범위하게 사용합니다. 이에 대해서는 여기에 썼습니다. 가산기와 마찬가지로 비트 이동 연산에는 두 개의 마이크로명령어가 필요합니다. 첫 번째는 시프터로 값을 옮기고 이동 방향과 양을 설정합니다. 두 번째 마이크로명령어는 시프터에서 목적지로 결과를 옮깁니다.
8087은 거의 모든 연산을 마이크로코드로 구현합니다. 기본 산술 연산(덧셈, 뺄셈, 곱셈, 나눗셈)은 모두 같은 마이크로코드를 실행하는 것으로 시작합니다(십진 주소 #0001). 마이크로코드는 두 인수를 임시 레지스터 tmpA와 tmpB로 옮깁니다. 보통 이 인수들은 스택 레지스터에서 가져옵니다. 메모리에서도 가져올 수 있지만 여기서는 다루지 않겠습니다.
다음으로 마이크로코드는 특정 연산 유형을 처리하기 위해 마이크로 서브루틴을 호출합니다. 8087의 마이크로코드 엔진에는 마이크로코드 점프 또는 서브루틴 호출의 대상인 주소 표를 담은 작은 ROM이 있습니다. 그러나 대상 0은 특별합니다. 곱셈, 나눗셈 또는 덧셈/뺄셈을 위한 서로 다른 세 주소를 갖습니다. 이는 ROM의 특수 사례 회로로 구현됩니다. 칩을 설계할 때라면 원한다면 이런 이상하고 임시방편적인 최적화를 할 수 있습니다.
덧셈/뺄셈 마이크로 서브루틴은 주소 #0232에서 시작하며, 이 마이크로코드는 아래에 나와 있습니다. 제어 흐름은 복잡하므로 각주에 순서도를 넣었습니다.5 마이크로코드는 먼저 0+0 또는 0-0인 0 사례를 처리합니다. 다음으로 두 인수의 지수를 뺍니다. 첫 번째 인수의 지수가 더 작으면(즉, 결과가 음수이면) 더 큰 인수가 먼저 오도록 인수를 교환합니다. 8087은 결과 부호를 “부호 래치”에 추적합니다. 인수를 교환하면 부호가 바뀌는 경우 #0244에서 래치를 토글합니다.
#0232 jmp #0236 if not tmpA:tag ZERO 0+0 확인
#0233 jmp #0236 if not tmpB:tag ZERO
#0234 return if not unsigned sub 0+0 같은 부호
#0235 jmp #0263 0+0 다른 부호 처리
#0236 tmpA sign -> sign latch nonzero: tmpA 부호를 부호 래치로
#0237 tmpB:exp -> Breg
#0238 adder: tmpA:exp - Breg cin=1 지수 비교
#0239 jmp #0245 if adder pos
#0240 sumreg:frac -> Breg tmpB의 지수가 더 크면 교환
#0241 adder: zero - Breg cin=1
#0242 xchg tmp
#0243 jmp #0245 if not unsigned sub
#0244 !sign latch -> sign latch 교환이 중요하면 부호 토글
#0245 xchg tmp 계속
#0246 shift zero R 0 bytes, 0 bits 0으로 설정
#0247 call CREATE_DENORM 지수에 맞게 tmpB 이동
#0248 xchg tmp
#0249 zero -> tmpB:exp 지수 조정을 tmpB:exp에 저장
#0250 jmp #0269 if not unsigned sub 기본 덧셈?
#0251 adder: tmpA:frac - Breg cin=1, roundmode tmpA - tmpB
#0252 jmp #0256 if adder pos
#0253 sumreg:frac,rnd -> Breg/rnd 가수 부정
#0254 adder: zero - Breg cin=1, roundmode
#0255 !sign latch -> sign latch 부호 토글
#0256 jmp #0275 if Sum register is zero 결과가 0이면 점프
#0257 shift R -> tmpB:frac 원래 tmpB, CREATE_DENORM에서 가져옴
#0258 jmp #0282 if tmpA top bit tmpA가 비정규가 아님?
#0259 jmp #0282 if tmpB:frac[63] tmpB가 비정규가 아님?
#0260 jmp #0289 둘 다 비정규이면 정규화 건너뜀
#0261 tmpA:exp -> expConv 0 처리
#0262 jmp #0264 if not tmpA top bit
#0263 zero -> tmpA 0 사례 처리
#0264 0 -> tmpA sign +0
#0265 zero -> tmpA:frac
#0266 return if not round chop
#0267 1 -> tmpA sign -0 반환
#0268 return
#0269 adder: tmpA:frac + Breg cin=0, roundmode 기본 덧셈: tmpA + tmpB
#0270 jmp #0289 if not sumreg[64] 오버플로 확인
#0271 shift 1 sumreg:sign,frac,rnd R 정규화를 위해 오른쪽으로 1비트 이동
#0272 shift R -> sumreg:frac,sign,rnd
#0273 expAdj++ 지수 조정 증가
#0274 jmp #0289 덧셈 마무리
#0275 shift sumreg:frac,rnd L 0 bytes, 3 bits 0? 반올림 비트를 얻기 위한 이동
#0276 shift L -> sumreg:frac
#0277 jmp #0261 if Sum register is zero 반올림 비트도 없으면 점프
#0278 zero -> sumreg:frac 결과 지우기
#0279 jmp #0289 if not tmpA top bit 비정규?
#0280 expAdj-- 비정규가 아니면 지수 아래로 조정
#0281 1 -> sumreg:frac,rnd 결과의 최하위 비트 설정
#0282 lzcount -> shiftcountH 선행 0 조정: 바이트 처리
#0283 shift sumreg:frac,rnd L count byte sticky
#0284 shift L -> sumreg:frac,rnd
#0285 lzcount -> shiftcountL 남은 선행 0 비트 처리
#0286 shift sumreg:frac,rnd L count bit sticky
#0287 shift L -> sumreg:frac,rnd
#0288 shiftcount -> tmpB:exp 비트 이동량 기억
#0289 sign latch -> tmpA sign 부호 저장
#0290 call ADJUST_PRECISION if not FBLD/FBSTP 필요하면 정밀도 조정
#0291 sumreg:frac -> tmpA:frac 조정된 가수 부분 저장
#0292 tmpB:exp -> Breg 지수 가져오기
#0293 adder: tmpA:exp - Breg cin=0 Breg로 조정
#0294 expAdj -> Breg
#0295 adder: sumreg:frac + Breg cin=0 expAdj로 조정
#0296 sumreg:frac -> expConv expConv의 지수로 오버플로 등 확인
#0297 sumreg:frac -> tmpA:exp tmpA에 지수 저장
#0298 return 완료
주소 #0247에서 중요한 정렬 단계가 수행됩니다. tmpB를 이동해 이진 소수점이 tmpA와 일렬로 맞춰집니다. 이는 마이크로코드 서브루틴 CREATE_DENORM으로 수행합니다. tmpB를 이동할 위치 수는 합 레지스터로 전달됩니다. 두 지수의 차이는 앞선 뺄셈에서 계산되었습니다. 차이가 음수이면 보수화했으므로 전달되는 값은 양수입니다.
일반적으로 결과의 지수는 더 큰 값의 지수와 같지만, 항상 그렇지는 않습니다. 지수의 변화는 두 곳에서 추적됩니다. 첫째, 제가 지수 조정(expAdj)이라 부르는 특수 2비트 레지스터가 한 비트씩 왼쪽 또는 오른쪽으로 이동하여 생기는 조정을 보관합니다. 하드웨어는 감소할 여지를 남기기 위해 expAdj 레지스터를 1로 초기화합니다. 더 큰 지수 변화는 tmpB 레지스터의 지수 필드에서 추적하며, 마이크로코드는 #0249에서 이를 0으로 초기화합니다.
주소 #0250에서 마이크로코드는 핵심 결정을 내립니다. 저수준 연산은 부호 없는 덧셈인가, 부호 없는 뺄셈인가? 관련된 부호에 따라 고수준 연산(덧셈 또는 뺄셈)은 저수준 연산과 일치하지 않을 수 있습니다. 예를 들어 2+3, -2-3, -2+(-3) 등에서는 두 부호 없는 숫자를 더합니다. 결과는 양수든 음수든 5입니다. 그러나 -2+3, 2-3, 3-2 등에서는 두 부호 없는 숫자를 뺍니다. 결과는 양수든 음수든 1입니다. 여기서는 부호 없는 크기를 더하는 전자의 경우를 “부호 없는 덧셈”, 부호 없는 크기를 빼는 후자의 경우를 “부호 없는 뺄셈”이라고 부르겠습니다. 핵심은 덧셈 명령어가 부호 없는 덧셈 또는 뺄셈을 초래할 수 있고, 뺄셈 명령어도 마찬가지라는 점입니다.
8087에는 저수준 부호 없는 연산이 덧셈인지 뺄셈인지 판단하는 특수 회로가 있어 조건문으로 이를 검사할 수 있습니다. tmpA와 tmpB 값은 음수일 때 1, 양수일 때 0인 부호 비트를 갖습니다. 뺄셈에 1, 덧셈에 0을 할당하면 tmpAsign⊕tmpBsign⊕op는 부호 없는 연산이 뺄셈일 때 1, 덧셈일 때 0을 산출합니다. 8087에는 부호가 같은지 다른지도 검사하는 조건문이 있습니다.
부호 없는 덧셈 경로가 더 간단하므로 주소 #0269에서 시작하는 이 경로부터 보겠습니다. 단계는 본질적으로 앞에서 설명한 것과 같습니다. 숫자를 정렬하고, 더하고, 정규화합니다. 먼저 tmpB에 있는 더 작은 숫자를 지수가 맞을 때까지 오른쪽으로 이동합니다. 그런 다음 부호 없는 값을 더합니다. 최상위 비트에서 캐리가 나오면 #0271에서 결과를 오른쪽으로 이동합니다. 결과를 오른쪽으로 한 비트 이동하므로 필요한 지수 조정을 추적하기 위해 특수 2비트 expAdj 카운터를 증가시킵니다.
#0289에서 결과의 부호는 부호 레지스터의 값으로 갱신됩니다. 8087은 80비트 임시 실수, 64비트 실수, 32비트 실수라는 세 가지 값 크기를 지원합니다. 더 작은 크기의 경우 마이크로코드 서브루틴 ADJUST_PRECISION이 값을 적절한 크기로 자릅니다. 결과를 올림하면 expAdj 레지스터에 기록된 결과 지수 조정이 필요할 수 있으므로, 이는 보기만큼 사소하지 않습니다. 마지막으로 결과의 지수는 초기 지수(tmpA에 있음)에서 tmpB 지수의 조정값(보통 0)을 빼고, expAdj 레지스터의 조정값을 더한 다음, 1을 뺀 값으로 계산됩니다(expAdj가 1에서 시작하기 때문입니다). 지수는 결과에 저장될 뿐 아니라 오버플로 또는 언더플로를 감지하기 위해 지수를 평가하는 회로인 “지수 변환기”에도 저장됩니다.
이 시점에서 덧셈/뺄셈 마이크로 서브루틴은 공통 마이크로코드로 돌아갑니다. 공통 마이크로코드는 오버플로 또는 언더플로가 있었는지 검사하며, 이는 값이 부동소수점 형식에 맞지 않음을 나타냅니다. 이 경우 값 처리를 위해 NONNORMAL_RESULT 서브루틴을 호출하며, 인터럽트 발생, 무한대 반환 또는 0 반환이 포함될 수 있습니다. 자세한 내용은 제 이전 글을 참조하세요.
결과는 적절한 스택 레지스터에 기록하여 스택에 푸시됩니다. 원래 명령어가 끝에서 스택 팝을 지정했다면 스택 최상단을 비어 있음으로 표시하고 스택 포인터를 갱신합니다. 마지막으로 다음 명령어 실행 마이크로명령어가 현재 명령어의 마이크로코드 끝을 나타냅니다. 부동소수점 명령어가 마침내 완료됩니다.
이제 #0251에서 시작하는 부호 없는 뺄셈 경로로 전환하겠습니다. 명령어는 예를 들어 -2 + 3처럼 덧셈일 수도 있음을 기억하세요. 마이크로코드는 64비트 가산기로 tmpA-tmpB라는 부호 없는 뺄셈을 수행합니다. 뺄셈을 수행하기 위해 두 번째 인수를 부정합니다. 결과는 어느 값이 더 큰지에 따라 양수 또는 음수일 수 있습니다. 이후 경로는 덧셈과 같습니다. 부호를 설정하고, 정밀도를 조정하며, 최종 지수를 계산합니다.
뺄셈 결과가 0이면 #0275는 세 개의 반올림 비트를 검사합니다. 반올림 비트가 설정되어 있으면 결과의 하위 비트를 설정하고, 이를 상쇄하기 위해 지수 조정을 1 감소시킵니다. 비정규값의 경우 반올림 비트를 승격할 “추가” 공간이 없으므로 결과를 지웁니다. 결과가 실제로 0인 경우에는 마이크로코드가 결과가 +0인지 -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 이동은 두 단계로 수행됩니다. 먼저 결과를 선행 0 바이트 수만큼 왼쪽으로 이동합니다. 다음으로 선행 0 회로는 남은 선행 0 비트 수를 셉니다. 이 시점에서 그 수는 0에서 7입니다. 결과는 그 비트 수만큼 왼쪽으로 이동합니다. 총 이동량은 tmpB의 지수에 저장됩니다.
부호 없는 뺄셈 경로는 앞서 설명한 부호 없는 덧셈 경로와 #0289에서 합류하여 정밀도 조정, 지수 계산, 결과 저장을 수행합니다. 그러면 마이크로 서브루틴은 곱셈과 나눗셈도 처리하는 공통 코드로 돌아갑니다.
8087은 무한대, 0, 숫자가 아님, 비정규값 및 기타 특수값에 관한 많은 특수 사례를 처리합니다. 8087의 설계자들은 이 특수 사례들을 신중히 고려하고 수학적으로 합리적인 방식으로 처리하려 했습니다.6 아래 표는 8087이 처리하는 특수 사례의 일부일 뿐입니다.
사용자 설명서에 설명된 무한대 특수 사례 처리의 일부.
이러한 특수 사례는 8087을 수학적으로 더 정확하게 만들지만, 마이크로코드는 훨씬 더 복잡해집니다. 아래 마이크로코드는 덧셈, 뺄셈, 곱셈, 나눗셈에 공통입니다. 앞서 설명한 대로 인수를 로드하고 결과를 저장하지만, 대부분은 특수 사례를 처리합니다. 이 마이크로코드는 수많은 특수 사례를 처리하기 위해 조건부 분기가 18개 연속으로 이어지는 “스파게티 코드”입니다. 여기서는 높은 수준에서 설명하겠습니다. 각주의 순서도7에 세부 사항이 있습니다.
#0001 call MEM_OP if mem op 메모리 연산이면 인수를 가져올 수 있음
#0002 jmp #0004 if mem op
#0003 st(i) -> tmpB 스택에서 가져오기
#0004 jmp #0018 if DE (denormalized) interrupt 비정규화 인터럽트면 종료
#0005 st(0) -> tmpA 스택 최상단을 tmpA로
#0006 jmp #0008 if not reverse bit FSUBR, FMUL, FDIVR이면 점프 안 함?
#0007 xchg tmp 인수 순서 반전
#0008 jmp #0019 if tmp anomalous 비정상 값 처리
#0009 call INSTR_SWITCH 명령어 유형 전환: J1(곱셈), J2(덧셈/뺄셈), J3(나눗셈)
#0010 call NONNORMAL_RESULT if not exp normal 복귀, 명령어 마무리
#0011 jmp #0014 if dst st(i) 목적지 d 비트 검사
#0012 tmpA -> st(0) 목적지 st(0)
#0013 RNI
#0014 tmpA -> st(i) 종료: 목적지 st(i)
#0015 jmp #0018 if not pop at end 팝이 아니면 RNI
#0016 empty -> SP(0) tag 스택 최상단을 비어 있음으로 표시
#0017 stackPtr++ 팝
#0018 RNI
#0019 call SPECIAL_TMPS 특수값 처리
#0020 jmp #0023 if not flag
#0021 jmp #0011 if expConv[0] 마무리로 점프
#0022 jmp #0009 명령어 유형 전환
#0023 cond3b -> sign latch 추가 조건
#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, 마무리
#0029 high bit -> tmpA:frac
#0030 sign latch -> tmpA sign
#0031 jmp #0037 마무리
#0032 jmp #0035 if not tmpA:tag ZERO 비정규/0으로 나누기
#0033 tmpB:frac -> sumreg:frac 0으로 나누기
#0034 jmp #0055 if not Sum register is zero
#0035 except:invalid
#0036 NaN -> tmpA
#0037 jmp #0018 if intr RNI로 점프
#0038 jmp #0011 마무리
#0039 jmp #0051 if not tmpA:tag SPECIAL 추가 조건
#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 ∞ ∞×∞: 아핀인지 검사
#0044 jmp #0035 if unsigned sub ∞ 덧셈 또는 뺄셈인지 검사
#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 0으로 나누기?
#0053 jmp #0028
#0054 jmp #0057 if not FDIV FDIVR
#0055 zero -> tmpA 0÷비정규 또는 n÷∞
#0056 jmp #0030 처리 후 마무리
#0057 xchg tmp
#0058 jmp #0061 if sign mismatch
#0059 jmp #0062 if unsigned sub 덧셈 또는 뺄셈인지 검사
#0060 jmp #0038 마무리
#0061 jmp #0038 if unsigned sub 마무리
#0062 tmpA sign -> sign latch 부호 보수
#0063 !sign latch -> tmpA sign
#0064 jmp #0038 마무리
#0008을 보면 8087 마이크로아키텍처에는 어느 임시 레지스터든 비어 있거나 특수값 태그가 붙었는지 확인하는 조건 검사가 포함되어 있습니다. 그렇다면 제가 SPECAL_TMPS라고 부르는 서브루틴을 호출합니다.8 이 서브루틴은 두 임시 레지스터를 검사하고 어떤 특수 처리가 필요한지 나타내는 두 플래그를 설정합니다.
두 플래그가 모두 설정되면 인수 중 하나에 빈 태그가 붙은 것이며, 보통 스택 언더플로를 나타냅니다. 이 경우 마이크로코드는 숫자가 아님(NaN)을 반환합니다. 두 번째 플래그가 클리어되어 있으면 인수 중 하나가 0에 매우 가까운 특수값인 비정규 숫자였습니다. 이 경우 산술 연산은 계속됩니다.
나머지 코드 경로는 복잡해집니다. 예를 들어 인수의 부호, 특정 산술 연산, 또는 인수가 0이나 무한대인지에 따라 분기합니다. 여덟 코드 경로는 숫자가 아님을 반환합니다. n÷비정규, 0÷0, 0÷의사 0, ∞×0, ∞÷∞, 0×∞, ∞ 연산 ∞ 사영, ∞-∞ 아핀입니다. 이들 중 다수는 간단합니다. 예를 들어 0×∞에는 의미 있는 값이 없습니다. 하지만 마지막 두 경우에는 조금 더 설명이 필요합니다.
8087에는 제어 레지스터의 비트로 선택하는 무한대의 두 가지 모드가 있습니다. “아핀 폐쇄”에서는 무한대에 부호가 있어 +∞와 -∞는 서로 다른 두 값입니다. 권장되는 모드는 무한대에 부호가 없는 “사영 폐쇄”입니다. 아핀 모드에서 ∞+∞는 양의 무한대를 산출하는 완전히 유효한 연산입니다. 그러나 ∞-∞는 의미가 없습니다. 0이라는 답은 임의적이므로 NaN을 반환합니다. 사영 모드에서는 무한대가 무한히 양수이면서 동시에 무한히 음수이므로 무한대에 대한 어떤 연산도 의미가 없습니다.
마이크로코드에는 무한대를 반환하는 서로 다른 여덟 경로가 있습니다. n÷0, ∞×n, ∞×∞, n×∞, ∞÷x, x±∞, ∞±x, ∞+∞ 아핀이 그것입니다. 첫 번째 경우는 0으로 나누기 예외가 마스킹되었다고 가정합니다. 처음 다섯 경우에서 마이크로코드는 인수들의 부호를 조합해 무한대의 부호를 생성합니다. 그러나 마지막 세 경우는 무한대 인수의 부호를 취하고 다른 부호는 무시합니다. 즉, 곱셈과 나눗셈은 무한대의 부호를 뒤집을 수 있지만 덧셈과 뺄셈은 그럴 수 없습니다.
두 경로는 특수 사례 0 결과를 다룹니다. 0÷비정규와 n÷∞입니다. 마이크로코드는 인수의 부호에 기반해 올바른 부호를 가진 0을 생성합니다.
뺄셈에서는 숫자 자릿수가 상쇄되어 이동으로 제거해야 하는 선행 0이 남을 수 있습니다. 8087의 시프터는 64비트 값을 임의의 비트 수만큼 이동할 수 있습니다. 선행 0 회로의 역할은 이동량을 결정하기 위해 선행 0을 세는 것입니다. 즉, 첫 번째 1 비트의 왼쪽에 있는 0의 개수를 세고 이를 이진값으로 변환합니다. 이는 기본적으로 첫 번째 1 비트의 위치를 부호화하는 우선순위 인코더와 같습니다.
선행 0 회로는 각 이진 출력 비트가 모든 입력 비트에 의존하므로 다소 복잡합니다. 64비트 검출기에는 64비트가 들어옵니다. 한 가지 접근법은 진리표를 취해 최적화하여 논리 회로 더미를 만드는 것입니다. 다른 접근법은 계층적 접근법, 즉 log(N) 단계의 트리를 사용하는 것입니다. 8087은 더 단순한 경로를 택합니다. 한 번의 패스로 바이트 단위로 선행 0을 제거하고, 두 번째 패스로 남은 0에서 7개의 선행 0 비트를 제거합니다. 각 패스에는 별도의 8입력 선행 0 검출기 회로가 필요합니다. 이 접근법은 더 느리지만 회로는 훨씬 단순합니다. 64비트 선행 0 검출기 하나 대신 8비트 선행 0 검출기 두 개를 사용합니다.
아래 회로도는 0에서 7개의 선행 0을 세 자리 이진값으로 변환하는 회로를 보여 줍니다. 회로도를 단순화하기 위해 첫 번째 논리 계층을 8비트 입력을 나타내는 비트마스크로 대체했습니다. 예를 들어 x01xxxxx는 두 번째 비트가 0이고 세 번째 비트가 1이며 나머지 비트는 임의일 때 신호가 높음을 나타냅니다. 텍스트 설명을 추가했지만 최적화 때문에 설명에는 몇 가지 예외가 있습니다. 예를 들어 x01xxxxx는 선행 0이 두 개인 모든 것과 일치합니다. 또한 선행 0이 없는 일부 패턴과도 일치하지만, 이들은 결과를 바꾸지 않습니다. 중요한 점은 선행 0이 두 개인 모든 경우와 일치하고 선행 0이 하나 또는 세 개인 경우와는 전혀 일치하지 않는다는 것입니다.
0에서 7개의 선행 0 비트를 검출하는 회로. 선행 0 바이트 검출에도 같은 접근법을 사용합니다.
회로의 출력은 선행 0의 개수를 나타내는 3비트 이진값입니다. 맨 위 NOR 게이트는 1, 3, 5 또는 7개의 선행 0에서 설정되는 하위 비트를 생성합니다. 상위 비트는 맨 아래 NOR 게이트가 생성하며 4, 5, 6 또는 7에서 설정됩니다. 중간 비트는 더 까다롭습니다. 선행 0이 4개 또는 5개인지(또는 어쩌면 0개나 1개인지)를 결정하기 위한 추가 논리 계층이 있습니다. 두 개의 1 사례는 0 네 개를 차단하고, 00 쌍은 선행 0 네 개가 적어도 6개로 바뀐다는 의미입니다. 이는 대략적인 설명이지만, 256개 사례를 열거하여 모두 올바른 답을 산출함을 검증할 수 있습니다.
선행 0이 있는 바이트 수를 세기 위해서는 비슷한 논리를 사용합니다. 하지만 8비트의 각 그룹은 바이트에 설정된 비트가 있는지 검출하기 위해 OR 게이트를 통과합니다. 따라서 이 논리는 선행 0 바이트가 몇 개 있는지 결정합니다.
다이에서 선행 0 논리는 합 레지스터 바로 왼쪽에 있으며 합 레지스터의 각 비트를 검사합니다.
대부분의 사람은 덧셈이나 뺄셈 같은 컴퓨터 연산에 큰 관심을 두지 않습니다. 하지만 그 이면에는 흥미로운 알고리즘과 특수 사례가 있으며, 이 기능들은 영리한 실리콘 회로로 구현됩니다. 양의 0과 음의 0, 양의 무한대와 음의 무한대, 숫자가 아님을 처리하는 복잡한 규칙은 오늘날 사용되는 IEEE-754 부동소수점 표준으로 이어진 8087 덕분에 현대 컴퓨터의 일부가 되었습니다. 그러나 8087은 특수 사례를 약간 지나치게 많이 넣었습니다. 사영 무한대, 비정규가 아닌 비정상값, 의사 0 같은 일부 8087 기능은 수고를 들일 가치가 없어서 표준에서 빠졌습니다.
8087 마이크로코드의 리버스 엔지니어링을 계속할 계획입니다. 새 소식을 보려면 Bluesky의 @righto.com, Mastodon의 @[email protected], 또는 RSS를 팔로우하세요. 저는 “Opcode Collective”의 구성원들, 특히 ROM 이미지를 마이크로코드 데이터로 변환하고 내용을 광범위하게 분석한 Smartest Blob과 Gloriouscow와 함께 이 작업을 해 왔습니다. 자세한 내용은 GitHub의 8087 저장소를 보세요. AI 관련 성명: 대시가 포함되어 있음에도 이 글 작성에는 AI를 사용하지 않았습니다(세부 사항). Opcode Collective는 각 ROM 셀을 분류하여 마이크로코드를 추출하는 데 ML 알고리즘을 사용했습니다.
Intel의 문서는 8087의 성능을 8086에서 에뮬레이션한 부동소수점 연산과 비교합니다. 가속 비율은 명령어에 따라 다르지만 보통 거의 100배입니다. 이 가속 덕분에 8087 칩은 CAD 및 스프레드시트 같은 수치 응용 프로그램에서 매우 바람직했습니다.↩
내부적으로 8087은 부동소수점 값을 보관하는 데 80비트를 사용합니다. 부호에 1비트, 지수에 15비트, 가수에 64비트입니다. 하지만 8087은 가드, 라운드, 스티키라고 부르는 반올림용 추가 하위 비트 세 개를 사용합니다. 이 비트들은 값이 항상 올바른 방향으로 반올림되도록 합니다. 데이터패스의 일부에는 부호 또는 오버플로를 위한 추가 비트가 있습니다. 시프터 폭은 68비트이며 가산기 폭은 69비트입니다. 대부분의 경우 이 추가 비트들은 무시하고 데이터패스를 64비트 폭이라고 부르겠습니다.↩
부동소수점 숫자는 일부 문제를 해결하지만 자체적인 문제도 도입합니다. 예를 들어 0.01은 부동소수점으로 정확히 표현할 수 없습니다. 1/3을 십진수로 정확히 표현할 수 없는 것처럼 이진수에서 무한 반복 소수이기 때문입니다. 부동소수점 숫자로 돈을 표현하고 센트 단위를 다룬다면 이는 큰 혼란을 일으킬 수 있습니다. 부동소수점의 문제에 관한 자세한 내용은 Julia Evans의 만화 또는 모든 컴퓨터 과학자가 부동소수점 산술에 관해 알아야 할 것을 참조하세요.↩
8087의 임시 실수 형식과 외부 형식의 한 가지 차이는 임시 실수에서는 선행 1 자릿값을 숫자에 명시적으로 저장한다는 점입니다. 외부 형식에서는 선행 1이 암묵적이므로 숫자에 저장하지 않습니다. 이로 인해 정확도 비트 하나를 추가로 제공하지만 산술은 더 복잡해집니다.↩
아래 순서도는 덧셈/뺄셈 마이크로코드의 제어 흐름을 보여 줍니다. 더 큰 이미지를 보려면 클릭하세요.
덧셈/뺄셈 마이크로코드 순서도.
William Kahan은 8087의 배후에 있는 부동소수점 전문가입니다. 예를 들어 NaN != NaN인 이유 같은 여러 설계 결정의 동기에 관해 알고 싶다면 그가 여기에 쓴 글을 참조하세요. 흥미로운 한 의견은 온라인에서 흔히 논의되는 0 0의 값을 다룹니다. Kahan은 “0, ∞ 및 NaN을 포함한 모든 x에 대해 x**0 = 1이라는 합의가 커지고 있다”고 말합니다.↩
아래 순서도는 기본 산술 명령어가 공유하는 마이크로코드를 보여 줍니다. 이 순서도는 위 순서도를 서브루틴으로 호출합니다. 수많은 특수 사례 경로에는 번호가 매겨져 있습니다.
기본 산술 연산을 위한 공통 마이크로코드의 순서도. 더 큰 이미지를 보려면 클릭하세요.
SPECIAL_TMPS의 동작은 아래에 요약되어 있습니다. 이 서브루틴은 flag 플립플롭과 지수 변환기 레지스터(expConv)를 통해 상태를 반환합니다. 동작은 다음과 같습니다.| 입력 | 결과 | flag | expConv |
|---|---|---|---|
| 비어 있음 | NaN, 예외 | 1 | 1 |
| NaN | 더 큰 NaN, 예외 | 1 | 1 |
| 무한대 | 무한대 | 0 | 1 |
| 비정규값 | 비정상값 | 1 | 0 |
| 0으로 나누기와 유사 | 변화 없음 | 0 | 0 |
제가 “0으로 나누기와 유사”라고 부른 줄은 나눗셈 명령어가 0 또는 0에 가까운 비정상 숫자로 나누려 함을 나타냅니다. 8087이 0과 0 근처의 값을 표현하는 방식에 관한 약간의 배경을 설명하겠습니다. 정상 부동소수점 값의 가수는 첫 비트가 설정되어 있습니다. 불행히도 정상 숫자에는 가장 작은 정상 숫자와 0 사이에 큰 간격이 있습니다. 비정규값은 이 “규칙”을 깨서 최상위 비트를 0으로 둘 수 있게 하므로 0 근처의 숫자가 훨씬 많아집니다. 10진법 비유로 1.003, 1.002, 1.001, 1.000을 거꾸로 센다고 해 봅시다. 최상위 숫자가 설정된 상태를 유지해야 한다면 다음 가능한 숫자는 0이며, 이는 1.000에서 엄청난 도약입니다. 비정규값은 0.999, 0.998, ...., 0.001, 0처럼 작은 단계로 계속 갈 수 있게 합니다.
비정규값의 지수는 가능한 가장 작은 값입니다. 비정상값은 비정규값과 비슷하지만 지수가 더 큰 값을 가집니다. 비정상값의 목적은 비정규화 예외가 마스킹되었을 때 비정규 숫자를 비정상값으로 변환하고 실행을 계속하는 것입니다.
진정한 0 값의 경우 8087은 부호 있는 0을 사용하므로 +0 또는 -0 둘 중 하나를 가질 수 있습니다. 진정한 0은 가능한 가장 작은 지수로 저장됩니다. 그러나 8087은 더 큰 지수를 갖는 0 값인 “의사 0”도 지원합니다. 의사 0은 아주 작은 비정상값 두 개를 곱하여 생길 수 있습니다. 80387 부동소수점 칩에서 Intel은 비정상값과 의사 0을 제거했습니다.
저는 앞서 SPECIAL_TMPS 서브루틴의 마이크로코드를 자세히 다뤘으므로 여기에서는 더 논의하지 않겠습니다.↩