HN 日本語サマリー

← 一覧へ戻る
科学・技術

Intel 8087チップ:浮動小数点加算と減算

The Intel 8087 chip: floating-point addition and subtraction (righto.com)

97 pointsby pwg33 コメント

要約

1980年に登場したIntel 8087は、浮動小数点演算を劇的に高速化したコプロセッサです。この記事では、8087のマイクロコードと回路を詳細に分析し、浮動小数点加算と減算がどのように実装されているかを解説します。特に、80ビットの内部フォーマット、レジスタスタック、および特殊値(無限大など)の処理に焦点を当て、その高性能の秘密に迫ります。

全文翻訳

浮動小数点加算と減算

1980年、IntelはIBM PCなどのシステムで浮動小数点演算を大幅に高速化するチップ、8087を導入しました。8087はタンジェント、平方根、対数などの高度な演算をサポートしていますが、この記事では基本的な算術演算である加算と減算に焦点を当てます。8087は8086プロセッサと比較して驚異的な速度向上を実現し、加算を約100倍、複雑な演算はそれ以上に高速化しました[1]。この記事では、8087のマイクロコードと回路を調査し、無限大やその他の特殊値に対する多数の特殊ケースを含め、浮動小数点加算と減算が正確にどのように実装されているかを説明します。

8087の回路を調査するため、チップの蓋をチゼルでこじ開け、顕微鏡を使用して以下の高解像度画像を作成しました。マイクロコードROMはダイの中央にある大きな長方形の領域で、チップを制御する1648個のマイクロ命令を保持しています。チップの下半分(赤い箱)はデータパスであり、80ビット値に対する浮動小数点計算を実行する回路です[2]。

[画像: 8087のデータパスのクローズアップ。加算と減算に使用される機能ブロックを示しています。この画像(または他の画像)をクリックすると拡大表示されます。]

A close-up of the 8087's datapath, showing functional blocks that are used for addition and subtraction. Click this image (or any other) for a larger version.

データパスを拡大すると、加算と減算に重要な機能ブロックが表示されます。主要なコンポーネントは中央にある整数加算器です。これは、Bレジスタから1つの引数を受け取り、一時レジスタやスタックレジスタなどのコンポーネントからもう1つの引数を受け取ります。加算器の出力は合計レジスタに格納されます。シフタは、値のビットを左または右にシフトできます。シフタはダイ上で多くのスペースを占有していますが、その回路により、値を任意のビット数だけ高速にシフトできます。浮動小数点演算では、値を整列するためにシフタが多用されます。先頭ゼロ回路は、値の左側にあるゼロビットまたはゼロバイトの数を数え、それらをシフトして除去できるようにします。指数コンバータは指数を処理し、オーバーフローとアンダーフローを検出します。これらの特殊回路は、8087が高いパフォーマンスを達成するのに役立ちました。

ほとんどのコンピュータは、数値を表現する基本的な方法として整数を使用します。しかし、多くのアプリケーションでは1.23のような小数部分を持つ数値を使用する必要があります。さらに、整数には非常に限られた範囲しかありません。32ビット整数は約20億から-20億までの数値を表現できます。これらの問題を解決するために[3]、コンピュータは通常、浮動小数点数を実装します。これは、広大な範囲にわたる小数を提供します。

ほとんどの場合、浮動小数点数は 符号 × 仮数部 × 2指数 で表現されます。仮数部は 1.bbb… という形式のバイナリ数です。つまり、先頭の1の後にバイナリ小数点(10進数の小数点に相当)があり、その後に残りのビットが続きます[4]。バイナリ数では、各ビットは2のべき乗を表します。小数部分、つまりバイナリ小数点より後の部分では、2のべき乗は負になります。2^-1, 2^-2 などです。したがって、バイナリ数1.1は1 + 1/2、つまり10進数で1.5です。同様に、バイナリ1.0011は1 + 1/8 + 1/16、つまり1.1875です。指数により、数値は仮数部の限られた範囲を超えることができます。たとえば、1.1(バイナリ)× 2^8 は384です。指数が大きくなると、浮動小数点数は巨大な範囲をカバーできます。32ビットの短い浮動小数点数でさえ、10^-36から10^38を超える範囲をカバーします。

浮動小数点加算と減算は、整数演算よりも複雑です。数値を加算(または減算)する前に、バイナリ小数点(10進数の場合と同様)を整列する必要があります。演算後、結果の指数を調整して仮数部を正規化し、先頭が1ビットになるようにします。

[画像: 簡単な浮動小数点加算の手順。]

The steps for a simple floating-point addition.

上の図は、簡単な浮動小数点加算 13.5+3.5 を示しています。最初の2行は、指数と仮数部で表現された2つの引数を示しています。両方の引数は正規化されているため、最も左のビットは1です。加算する前に、2番目の引数を2桁右にシフトして整列する必要があります(この場合、指数の差は2です)。次に、整列された仮数部に対して整数加算を使用して値を加算します。この場合、値を加算するとオーバーフローが発生し、左側に「余分な」ビットが生じます。結果は1桁右にシフトし、指数を1増やして正規化する必要があります。

浮動小数点減算も同様ですが、さらにいくつかの複雑さが伴います。結果は正、負、またはゼロになる可能性があります。結果は、引数よりも桁違いに小さくなる可能性があります。下の図は、基本的な減算 8-7.5 を示しています。加算と同様に、2番目の引数を右にシフトして指数を一致させます(つまり、バイナリ小数点を行に合わせます)。2つの仮数部は整数減算で減算されます。結果は左にシフトして正規化され、最上位ビットが1になります。加算の場合とは異なり、正規化には大きなシフトが必要になる場合があります。この場合、シフトは4ビットで、指数は4減算されて結果が得られます。

[画像: 簡単な浮動小数点減算。]

A simple floating-point subtraction.

減算の1つの複雑さは、結果が負になる可能性があることです。その場合、仮数部は正になり、結果の符号が反転されます。減算はゼロになることもあります。この場合、正規化は実行できません。代わりに、適切な値として特殊値+0または-0が返されます。

8087の関連ハードウェア詳細

このセクションでは、加算と減算のマイクロコード実装において重要な役割を果たす8087のいくつかの機能について説明します。

8087はさまざまなデータ型をサポートしていますが、内部的には「テンポラリ・リアル」と呼ばれる80ビット形式で数値を格納します。テンポラリ・リアル数は、符号ビット、15ビットの指数、および64ビットの仮数部で構成されます。15ビットの指数により、10^4932を超える異常に大きな数値を扱えます。64ビットの仮数部は、非常に精度の高い数値をサポートします。指数は「バイアス」として16383が加算されて格納されます。したがって、実際の指数が負であっても、格納される指数は常に正になります。たとえば、指数1は0x4000として格納され、指数-16は0x3fefとして格納されます。

[画像: 80ビットのテンポラリ・リアル形式とレジスタ形式。ドットは10進数の小数点に類似したバイナリ小数点を示します。]

The 80-bit temporary real format and the register formats. The dot indicates the binary point, analogous to the decimal point.

8087には8つの内部レジスタがあり、スタックとして編成されています。プログラマは8087をプログラミングする際にこれらのレジスタを使用します。各レジスタは80ビットの浮動小数点数を保持します。パフォーマンスを最適化するために、レジスタスタック内の各値には、ゼロ、有効、特殊、または空のいずれかの「タグ」値が関連付けられています。ゼロの値はzeroとしてタグ付けされます。「正規」の浮動小数点値はvalidとしてタグ付けされます。値が無限大、非数(NaN)、または非正規化値の場合、specialとしてタグ付けされます。最後に、emptyタグはレジスタが値を保持していないことを示します。これにより、スタックアンダーフロー(空のレジスタの読み取り)またはスタックオーバーフロー(空でないレジスタへの格納)を検出できます。

8087には、内部で使用される一時レジスタもあります。tmpAとtmpBは80ビットレジスタで、2つのタグビットが付いています。ただし、tmpCは異なります。符号、指数、タグはありません。さらに、仮数部は68ビットです。tmpCにはオーバーフローを防ぐための追加ビットが1つと、ガード、ラウンド、スティッキーとして知られる丸め処理のための3つの追加下位ビットがあります。

マイクロコードでは、各16ビットのマイクロ命令が1つの低レベル操作を実行します。多くの操作は、あるレジスタから別のレジスタへデータを移動します。他の操作は、条件付きジャンプ、サブルーチン呼び出し、およびリターンを提供します。

8087の処理の心臓部は69ビット整数加算器であり、これはこちらで説明しました。(加算器には、3つの丸めビットとオーバーフローおよび乗算のためのいくつかの追加上位ビットがあるため、64ビットよりも多くのビットがあります。)整数加算は2つのマイクロ命令を必要とします。最初のマイクロ命令は、加算する最初の引数を指定し、キャリーや丸めなどのさまざまなオプションを選択します。(減算の場合、オプションは2番目の引数を補数化します。)加算器の2番目の引数は常にBレジスタから取得されます。これは紛らわしいことにtmpBレジスタとは無関係です。加算操作の結果は、合計レジスタと呼ばれる特殊なレジスタに配置されます。2番目のマイクロ命令(最初の命令から数命令後になる場合があります)は、結果を合計レジスタから宛先に移動します。

浮動小数点演算では、ビットシフタも広範囲に使用されます。これは68ビットの値を任意の数の位置に左または右にシフトします。シフタについてはこちらで説明しました。加算器と同様に、ビットシフト操作は2つのマイクロ命令を必要とします。最初の命令は値をシフタに移動し、シフトの方向と量を設定します。2番目のマイクロ命令は、結果をシフタから宛先に移動します。

加算/減算マイクロコード

8087は、ほぼすべての操作をマイクロコードで実装しています。基本的な数学演算(加算、減算、乗算、除算)は、同じマイクロコード(10進数アドレス#0001)を実行することから始まります。マイクロコードは、2つの引数を一時レジスタtmpAとtmpBに移動します。通常、これらの引数はスタックレジスタから取得されます。引数はメモリから取得することもできますが、ここでは説明しません。

次に、マイクロコードはマイクロサブルーチンを呼び出して、特定の操作タイプを処理します。8087のマイクロコードエンジンには、アドレスのテーブルを保持する小さなROMがあり、マイクロコードジャンプまたはサブルーチン呼び出しのターゲットとなります。ただし、ターゲット0は特別です。乗算、除算、加算/減算の3つの異なるアドレスがあります。(これはROM内の特殊ケース回路で実装されています。チップを設計するときは、このような奇妙でハック的な最適化を行うことができます。)

加算/減算マイクロサブルーチンはアドレス#0232から始まります。このマイクロコードを以下に示します。制御フローは複雑なので、脚注にフローチャートを作成しました[5]。マイクロコードはまずゼロケース(0+0または0-0)を処理します。次に、2つの引数の指数を減算します。最初の引数の指数が小さい場合(つまり、結果が負になる場合)、引数がスワップされ、指数が大きい方の引数が先になります。(8087は結果の符号を「符号ラッチ」に保持します。引数を交換すると符号が変わる場合、ラッチは#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で、重要な整列ステップが行われ、tmpBがシフトされてバイナリ小数点がtmpAと一致するようになります。これはマイクロコードサブルーチンCREATE_DENORMによって行われます。tmpBをシフトする位置の数は合計レジスタで渡されます。2つの指数の差が計算されました(差が負の場合、補数化されたため、渡される値は正です)。

通常、結果の指数は(大きい方の)値の指数と同じですが、常にそうとは限りません。指数への変更は2つの場所で追跡されます。まず、私が指数調整(expAdj)と呼ぶ特殊な2ビットレジスタは、1ビットの左または右シフトによる調整を保持します。ハードウェアは、デクリメントのためのスペースを確保するために、expAdjレジスタを1で初期化します。より大きな指数変更は、tmpBレジスタの指数フィールドで追跡されます。マイクロコードは、#0249でこれを0で初期化します。

アドレス#0250で、マイクロコードは重要な決定を下します。低レベル操作は符号なし加算か、それとも符号なし減算か? 含まれる符号によっては、高レベル操作(加算または減算)が低レベル操作と一致しない場合があります。たとえば、2+3、-2-3、-2+(-3)などの場合、2つの符号なし数値が加算され、結果は5(正または負)になります。しかし、-2+3、2-3、3-2などの場合、2つの符号なし数値が減算され、結果は1(正または負)になります。ここでは、符号なし絶対値が加算されるケースを「符号なし加算」、符号なし絶対値が減算されるケースを「符号なし減算」と呼びます。重要な点は、加算命令が符号なし加算または減算の結果になり、減算命令も同様であるということです。

8087には、低レベルの符号なし操作が加算か減算かを決定する特殊な回路があり、条件付きでテストできます。tmpAとtmpBの値には、負の場合は1、正の場合は0の符号ビットがあります。減算に1、加算に0を割り当てると、tmpAsign⊕tmpBsign⊕opは、符号なし操作が減算の場合は1、加算の場合は0を生成します。8087には、符号が同じか異なるかをテストする条件もあります。

符号なし加算

符号なし加算パスはより単純なので、アドレス#0269から始めます。手順は基本的に前述の通りです。数値を整列し、加算し、正規化します。まず、小さい方の数値(tmpB内)が指数が一致するまで右にシフトされます。次に、(符号なし)値が加算されます。最上位ビットからのキャリーがある場合、結果は#0271で右にシフトされます。結果が1ビット右にシフトされるため、特殊な2ビットexpAdjカウンタがインクリメントされ、必要な指数調整が追跡されます。

#0289で、結果の符号が符号レジスタの値で更新されます。8087は3つのサイズの値をサポートしています。80ビットテンポラリ・リアル、64ビット・リアル、または32ビット・リアルです。より小さいサイズの場合、マイクロコードサブルーチンADJUST_PRECISIONが値を適切なサイズにトリミングします。(これは、結果を丸め上げると結果の指数に調整が必要になる場合があるため、単純ではありません。この調整はexpAdjレジスタに記録されます。)最後に、結果の指数は、初期指数(tmpA内)から、tmpBの指数内の調整値(通常は0)を減算し、expAdjレジスタ内の調整値を加算し、1を減算することによって計算されます(expAdjは1から始まるため)。指数は結果に格納されるだけでなく、「指数コンバータ」にも格納されます。これは、オーバーフローやアンダーフローを検出するために指数を評価する回路です。

この時点で、加算/減算マイクロサブルーチンは共通マイクロコードに戻ります。共通マイクロコードは、オーバーフローまたはアンダーフローがあったかどうかを確認し、値が浮動小数点数に収まらないことを示します。この場合、サブルーチンNONNORMAL_RESULTが呼び出され、値が処理されます。これには、割り込みのトリガー、無限大の返却、またはゼロの返却が含まれる場合があります。(詳細は、私の以前の記事を参照してください。)

結果は、適切なスタックレジスタに書き込むことによってスタックにプッシュされます。元の命令が最後にスタックポップを指定した場合、スタックトップは空としてマークされ、スタックポインタが更新されます。最後に、Run Next Instructionマイクロ命令は、現在の命令のマイクロコードの終了を示します。浮動小数点命令は最終的に完了しました。

符号なし減算

次に符号なし減算パスに切り替えます。#0251から始まります。(命令は加算である可能性があることに注意してください。たとえば、-2 + 3。)マイクロコードは64ビット加算器を使用して符号なし減算 tmpA-tmpB を実行します。2番目の引数は減算を実行するために否定されます。結果は、どちらの値が大きいかによって、正または負になります。残りのパスは加算と同じです。符号の設定、精度の調整、および最終指数の計算です。

減算結果がゼロの場合、#0275は3つの丸めビットをテストします。丸めビットが設定されている場合、結果の最下位ビットが設定され、指数調整はこれを相殺するために1減算されます。(非正規化数の場合、丸めビットを昇格させる「余分な」スペースがないため、結果はクリアされます。)結果が本当にゼロの場合、特別なパス#0261があります。これは、マイクロコードが+0と-0のどちらを返すかを決定する必要があるためです。0-0の減算は、+0および/または-0の加算または減算のさまざまな可能性を処理するために、このコードパスに結合されます。一般に、正と負のゼロは、一方に「傾いた」ゼロから期待されるとおりに機能します。たとえば、⁺0 + ⁺0 は ⁺0 であり、⁻0 - ⁺0 は ⁻0 です。しかし、ゼロが相殺される場合はどうでしょうか?たとえば、⁺0 + ⁻0 または ⁻0 - ⁻0?これらの場合、下位への丸めは⁻0を生成し、他の丸めモードは⁺0を生成します。

先頭ゼロ回路は、先頭ゼロの数を数えて、シフト量を決定します。回路を管理しやすくするために、先頭ゼロシフトは2つのステップで行われます。まず、結果がバイト単位で先頭ゼロの数だけ左にシフトされます。次に、先頭ゼロ回路は、残りの0から7までの先頭ゼロビットの数を数えます。結果はそのビット数だけ左にシフトされます。合計シフト量はtmpBの指数に格納されます。

符号なし減算パスは、前述の符号なし加算パスと#0289でマージされ、精度を調整し、指数を計算し、結果を格納します。その後、マイクロサブルーチンは、乗算と除算も処理する共通コードに戻ります。

共通マイクロコードにおける特殊ケース処理

8087は、無限大、ゼロ、非数(NaN)、非正規化数、その他の特殊値に対する多くの特殊ケースを処理します。8087の設計者はこれらの特殊ケースを慎重に検討し、数学的に妥当な方法で処理しようとしました[6]。下の表は、8087が処理する特殊ケースの一部にすぎません。

[画像: 「ユーザーズマニュアル」に記載されている無限大の特殊ケース処理の一部。]

Part of the special-case handling of infinities as described in the User's Manual.

これらの特殊ケースは8087を数学的に正確にしますが、マイクロコードをはるかに複雑にします。以下のマイクロコードは、加算、減算、乗算、除算で共通です。前述のように引数をロードし、結果を格納しますが、ほとんどの場合、特殊ケースを処理します。このマイクロコードは「スパゲッティコード」であり、多数の特殊ケースを処理するために18個の条件付き分岐が連続しています。ここでは高レベルで説明します。脚注のフローチャート[7]に詳細が記載されています。

#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を見ると、8087のマイクロアーキテクチャには、一時レジスタのいずれかが空であるか、特殊値でタグ付けされているかをチェックする条件付きテストが含まれています。もしそうであれば、私がSPECAL_TMPSと呼ぶサブルーチンが呼び出されます[8]。これは2つのテンポラリ・レジスタを調べ、どのような特殊処理が必要かを示す2つのフラグを設定します。

両方のフラグが設定されている場合、引数の1つが空としてタグ付けされており、通常はスタックアンダーフローを示します。この場合、マイクロコードは非数(NaN)を返します。2番目のフラグがクリアされている場合、引数の1つは非正規化数(ゼロに非常に近い特殊値)でした。この場合、算術演算は続行されます。

残りのコードパスは複雑になり、たとえば、引数の符号、特定の算術操作、または引数がゼロまたは無限大であるかどうかに分岐します。8つのコードパスが非数(NaN)を返します。n÷denorm、0÷0、0÷疑似ゼロ、∞×0、∞÷∞、0×∞、∞ op ∞ プロジェクティブ、および∞-∞ アファインです。これらの多くは単純です(たとえば、0×∞には意味のある値がありません)が、最後の2つはもう少し説明が必要です。

8087には無限大の2つの異なるモードがあり、制御レジスタのビットで選択されます。「アファイン閉包」では、無限大は符号付きであり、+∞と-∞は異なる値です。推奨されるモードは「プロジェクティブ閉包」であり、無限大は符号なしです。アファインモードでは、∞+∞は完全に有効であり、正の無限大を生成します。しかし、∞-∞は無意味です。0という答えは恣意的になるため、NaNが返されます。プロジェクティブモードでは、無限大の操作は意味がありません。なぜなら、無限大は無限に正であり、無限に負でもあるからです。

マイクロコードには、無限大を返す8つの異なるパスがあります。n÷0、∞×n、∞×∞、n×∞、∞÷x、x±∞、∞±x、および∞+∞ アファインです。(最初のケースは、ゼロ除算例外がマスクされていることを前提としています。)最初の5つのケースでは、マイクロコードは引数の符号を組み合わせて無限大の符号を生成します。しかし、最後の3つのケースでは、無限大の引数から符号を取得し、他の符号は無視します。つまり、乗算と除算は無限大の符号を反転させることができますが、加算と減算はできません。

2つのパスは、特殊ケースのゼロ結果を扱います。0÷denormとn÷∞です。マイクロコードは、引数の符号に基づいて、正しい符号を持つゼロを生成します。

先頭ゼロ回路

減算では、桁が相殺されて先頭ゼロが残ることがあり、これらは値をシフトして除去する必要があります。8087のシフタは、64ビット値を任意のビット数だけシフトできます。先頭ゼロ回路の仕事は、先頭ゼロを数えてシフト量を決定することです。つまり、最初の1ビットの左側にあるゼロの数を数え、それをバイナリ値に変換します。これは基本的に、最初の1ビットの位置をエンコードする優先度エンコーダと同じです。

先頭ゼロ回路は、各バイナリ出力ビットがすべての入力ビットに依存するため、やや複雑です。64ビット検出器には64ビットの入力があります。1つのアプローチは、真理値表を取得して最適化し、多数のロジックを生成することです。もう1つのアプローチは、階層的なアプローチ、つまりlog(N)ステージのツリーを使用することです。8087はより単純なパスを採用しています。バイト単位で先頭ゼロを除去する1パスと、残りの0から7までの先頭ゼロビットを除去する2番目のパスです。各パスには、個別の8入力先頭ゼロ検出回路が必要です。このアプローチは遅いですが、回路ははるかに単純です。64ビット先頭ゼロ検出器の代わりに、2つの8ビット先頭ゼロ検出器を使用します。

下の回路図は、0から7までの先頭ゼロビットを3ビットのバイナリ値に変換する回路を示しています。回路図を簡略化するために、最初のロジック層をビットマスクに置き換えました。これは8ビット入力表しています。たとえば、x01xxxxxは、信号が2番目のビットが0、3番目のビットが1、残りのビットが任意である場合に高くなることを示します。テキストの説明を追加しました。最適化のため、説明にはいくつかの例外があります。たとえば、x01xxxxxは2つの先頭ゼロを持つすべてに一致します。先頭ゼロがないパターンにも一致しますが、それらは結果を変更しません。重要なのは、2つの先頭ゼロを持つすべてに一致し、1つまたは3つの先頭ゼロを持ついずれにも一致しないことです。

[画像: 0から7までの先頭ゼロビットを検出する回路。先頭ゼロバイトを検出するためにも同じアプローチが使用されます。]

The circuitry to detect 0 to 7 leading zero bits. The same approach is used to detect leading zero bytes.

回路からの出力は、先頭ゼロの数を表す3ビットのバイナリ値です。最上位のNORゲートは、1、3、5、または7の先頭ゼロで設定される低ビットを生成します。高ビットは最下位のNORゲートによって生成されます。4、5、6、または7で設定されます。中央のビットはより複雑で、追加のロジック層があり、4つまたは5つの先頭ゼロ(またはおそらく0または1)があるかどうかを判断します。2つの1ケースは4つのゼロをブロックしますが、00ペアは4つの先頭ゼロが少なくとも6になることを意味します。(これは手短な説明ですが、256ケースを列挙してすべてが正しい答えを生成することを確認できます。)

先頭ゼロのバイト数を数えるために、同様のロジックが使用されます。ただし、各8ビットのグループはORゲートを通過して、バイトにセットされたビットがあるかどうかを検出します。したがって、ロジックは先頭ゼロのバイト数を決定します。

ダイ上では、先頭ゼロロジックは合計レジスタのすぐ左にあり、合計レジスタの各ビットを調べています。

結論

ほとんどの人は、コンピュータの加算や減算などの操作についてあまり考えません。しかし、その背後には興味深いアルゴリズムと特殊ケースがあり、これらの機能は巧妙なシリコン回路に具体化されています。正と負のゼロ、正と負の無限大、および非数を処理するための複雑なルールは、8087が今日のIEEE-754浮動小数点標準につながったため、現代のコンピュータの一部となっています。しかし、8087は特殊ケースにおいて少しやりすぎました。プロジェクティブ無限大、非正規化数、疑似ゼロなどの一部の8087機能は、労力に見合わないため、標準から削除されました。

8087のマイクロコードのリバースエンジニアリングを続ける予定です。最新情報については、Bluesky(@righto.com)、Mastodon(@kenshirriff@oldbytes.space)、またはRSSでフォローしてください。この作業は「Opcode Collective」のメンバー、特にROMイメージをマイクロコードデータに変換し、内容を広範囲に分析したSmartest BlobとGloriouscowと協力して行いました。詳細については、GitHubの8087リポジトリを参照してください。AIステートメント:エマーダッシュが存在するにもかかわらず、この記事の執筆にAIは使用されていません(詳細)。Opcode Collectiveは、MLアルゴリズムを使用して各ROMセルを分類し、マイクロコードを抽出しました。

注釈と参考文献

  1. Intelのドキュメントは、8087と8086でエミュレートされた浮動小数点演算のパフォーマンス比較を提供しています。速度向上は命令によって異なりますが、通常は100倍近くになります。この速度向上により、8087チップはCADやスプレッドシートなどの数値アプリケーションで非常に望ましいものとなりました。↩

  2. 内部的には、8087は浮動小数点値を保持するために80ビットを使用します。符号に1ビット、指数に15ビット、仮数部に64ビットです。しかし、8087は丸め処理のために3つの追加の下位ビット(ガード、ラウンド、スティッキーと呼ばれる)を使用します。これらのビットは、値が常に正しい方向に丸められることを保証します。データパスの一部には、符号またはオーバーフローのための追加ビットがあります。シフタは68ビット幅で、加算器は69ビット幅です。ほとんどの場合、これらの追加ビットは無視し、データパスを64ビット幅として参照します。↩

  3. 浮動小数点数は一部の問題を解決しますが、それ自体も問題を引き起こします。たとえば、0.01は浮動小数点数で正確に表現できません(10進数で1/3が正確に表現できないのと同様に、バイナリでは無限に繰り返される分数になります)。これは、浮動小数点数でお金を表現し、セントを扱っている場合に混乱を招く可能性があります。浮動小数点数の問題の詳細については、Julia Evansのコミックまたはすべてのコンピュータサイエンティストが浮動小数点演算について知っておくべきことを参照してください。↩

  4. 8087のテンポラリ・リアル形式と外部形式の1つの違いは、テンポラリ・リアルでは、先頭の1桁が数値に明示的に格納されていることです。外部形式では、先頭の1は暗黙的であり、数値に格納されないため、1ビットの精度が向上しますが、演算はより複雑になります。↩

  5. 下のフローチャートは、加算/減算マイクロコードの制御フローを示しています。(クリックすると拡大表示されます。)

[画像: 加算/減算マイクロコードのフローチャート。]

Flowchart of the add/sub microcode.

↩

  1. William Kahanは、8087の背後にある浮動小数点のエキスパートです。彼は、たとえばNaN != NaNの理由などの設計上の決定の動機についてこちらで書いています。(興味深いコメントとして、オンラインで一般的に議論されている00の値について触れています。Kahanは、「0、∞、NaNを含むすべてのxに対してx

0 = 1であるというコンセンサスが広まっている」と述べています。)↩

  1. 下のフローチャートは、基本的な算術命令で共有されるマイクロコードを示しています。このフローチャートは、上記のフローチャートをサブルーチンとして呼び出します。多数の特殊ケースパスには番号が付けられています。

[画像: 基本的な算術演算の共通マイクロコードのフローチャート。(クリックすると拡大表示されます。)]

Flowchart of the common microcode for basic arithmetic operations. (Click for a larger image.)

↩

  1. SPECIAL_TMPSのアクションを以下にまとめます。これは、flagフリップフロップと指数コンバータレジスタ(expConv)を通じてステータスを返します。そのアクションは次のとおりです。
入力結果flagexpConv
emptyNaN, exception11
NaN(larger) NaN, exception11
infinityinfinity01
denormunnorm10
divide-by-zero(ish)no change00

私が「divide-by-zero(ish)」と呼ぶ行は、除算命令がゼロまたは非正規化数(ゼロに近い数)で除算しようとしていることを示します。8087が0および0に近い値をどのように表現するかについて、背景を少し説明します。正規の浮動小数点数は、最上位ビットがセットされた仮数部を持ちます。残念ながら、正規数には最小の正規数とゼロの間に大きなギャップがあります。非正規化数はこの「ルール」を破り、最上位ビットがゼロになることを許可するため、0に近い数値がさらに多くなります。(10進数のアナロジーとして、1.003、1.002、1.001、1.000とカウントダウンすると想像してください。最上位桁がセットされなければならない場合、次に可能な数値は0であり、1.000から大きなジャンプになります。非正規化数は、小さなステップで続行することを許可します:0.999、0.998、...、0.001、0。)

非正規化数の場合、指数は可能な限り最小の値になります。非正規化数は非正規化数に似ていますが、指数がより大きな値になります。非正規化数の目的は、非正規化例外がマスクされている場合、非正規化数が非正規化数に変換され、実行が継続されることです。

真のゼロ値の場合、8087は符号付きゼロを使用するため、+0または-0のいずれかを持つことができます。真のゼロは、可能な限り最小の指数で格納されます。しかし、8087は「疑似ゼロ」もサポートしています。これは、より大きな指数を持つゼロ値です。疑似ゼロは、2つの非常に小さい非正規化数を乗算した結果として生じることがあります。(386浮動小数点チップでは、Intelは非正規化数と疑似ゼロを廃止しました。)

SPECIAL_TMPSサブルーチンのマイクロコードについては、以前に詳細を説明したため、ここではこれ以上説明しません。↩