HN 日本語サマリー

← 一覧へ戻る
科学・技術

Intel 8087浮動小数点チップのマイクロコード:SCALE命令

Microcode in Intel's 8087 floating-point chip: the scale instruction (righto.com)

114 pointsby pwg37 コメント

要約

この記事は、1980年に登場したIntel 8087浮動小数点コプロセッサのFSCALE命令のマイクロコードをリバースエンジニアリングした結果を解説しています。FSCALE命令は、浮動小数点数を2のべき乗でスケーリングする高速な命令ですが、その内部実装は予想以上に複雑で、多くの特殊ケースを処理するための140以上のマイクロ命令と3段階のサブルーチンコールを使用しています。

全文翻訳

8087のマイクロコード

1970年代、浮動小数点演算は混沌としていました。コンピュータメーカーは十数もの互換性のない算術標準を持っており、浮動小数点システムは数学的な厳密さよりもハードウェアの単純さを優先して設計されていたため、数値的安定性の問題が生じていました。この状況は、1980年にIntelが8087浮動小数点コプロセッサチップを発売したことで変化しました。このチップは、コーナーケースでも可能な限り正確になるように設計されていました。8087は、IBM PCに搭載可能であったため、表計算ソフトからCADまで幅広いアプリケーションで浮動小数点演算を最大100倍高速化できることから人気を博しました。しかし、より重要なのは、8087が今日のほとんどのコンピュータで使用されている浮動小数点標準となったことです。

8087は、マイクロコードと呼ばれる複雑な低レベルコードで命令を実装していました。私は、このマイクロコードをリバースエンジニアリングしているグループ「Opcode Collective」の一員であり、最近いくつかの進展がありました。この記事では、8087の命令の一つであるFSCALEのマイクロコードを調べ、その仕組みを説明します。FSCALE(Floating-point Scale)命令は、数値を2のべき乗でスケーリングする簡単な方法を提供し、乗算よりもはるかに高速です。私は、FSCALEは単純でほぼ些細な命令であり、理解して説明するのは容易だろうと考えていました。ネタバレ:単純ではありません。FSCALEは、多くの特殊ケースを処理するために140以上のマイクロ命令と3段階のサブルーチン呼び出しを使用します。しかし、FSCALEのマイクロコードは、シフター、加算器、指数コンバーターなど、8087の多くの興味深い部分を例示しており、また8087の隠された機能も明らかにしているので、興味を持っていただけることを願っています。

マイクロコードを調査するために、私は8087チップを開封し、顕微鏡で高解像度の画像を作成しました。中央にある大きなマイクロコードROMには、チップを制御する1648個のマイクロ命令が格納されています。左側にあるマイクロコードエンジンは、マイクロコードをステップ実行し、ジャンプやサブルーチン呼び出しを処理します。チップの下半分は「データパス」であり、浮動小数点計算を実行する回路です。これは、数値の指数用の16ビットデータパスと、数値の仮数(小数部とも呼ばれる)用の64ビットデータパスに分かれています。

[画像: Intel 8087浮動小数点ユニットチップのダイ(Die)で、主要な機能ブロックにラベルが付いています。ダイのサイズは5mm×6mmです。クリックすると大きな画像が表示されます。]

Intel 8087浮動小数点ユニットチップのダイ(Die)で、主要な機能ブロックにラベルが付いています。ダイのサイズは5mm×6mmです。クリックすると大きな画像が表示されます。

チップの下部を拡大するとデータパス回路が表示されます。関連部分を以下にハイライトしました。1 指数ROMには様々な定数が格納されています。指数コンバーターは、指数を検査し、特殊な値を検出し、指数フォーマット間を変換する特殊な回路です。2 シフターは大きなコンポーネントであり、64ビット3の値を任意の量だけ左または右にシフトできます。(8087のシフター回路についてはこちらで書きました。)加算器は8087の計算の中心であり、乗算、除算、平方根のためにループで使用されます。Bレジスタは加算器の一方の入力を保持し、複数のソースがもう一方の入力を提供できます。Sumレジスタは加算器の出力を保持します。8つのスタックレジスタと一時レジスタは浮動小数点数を保持します。

[画像: FSCALEによって使用される機能ブロックを示す、8087のデータパスのクローズアップ。]

FSCALEによって使用される機能ブロックを示す、8087のデータパスのクローズアップ。

8087の詳細

このセクションでは、FSCALEマイクロコードにとって重要な8087のいくつかの機能について説明します。8087を使用するために、プログラマーはスタックとして整理された8つの内部レジスタに値を格納します。各レジスタは80ビットの浮動小数点数を保持します。パフォーマンスを最適化するために、レジスタスタック内の各値には関連付けられた「タグ」値があり、これはプログラマーにはほとんど見えません。4 タグは値を有効、特殊、ゼロ、または空としてラベル付けします。「正規」の浮動小数点値は有効としてタグ付けされます。浮動小数点値が無限大、非数(NaN)、または非正規化数である場合、特殊としてタグ付けされます。ゼロ値はゼロとしてタグ付けされます。最後に、レジスタが空の場合(例:スタックから値がポップされた場合)、レジスタは空としてタグ付けされます。

8087には、内部的に使用される一時レジスタtmpA、tmpB、tmpCもあります。スタックレジスタと同様に、tmpAとtmpBは2つのタグビットを持つ80ビットレジスタです。しかし、tmpCは64ビットの仮数のみを保持します。

8087は、様々なサイズの浮動小数点数、整数、および2進化10進数など、さまざまなデータ型をサポートしています。しかし、内部的にはすべて「一時実数(temporary real)」と呼ばれる80ビット浮動小数点数として格納されます。この記事の残りの部分では、一時実数値のみを考慮します。数値は、符号ビット、15ビット指数、および64ビット仮数(小数部)の3つの部分から構成されます。ほとんどの場合、浮動小数点数は符号 × 仮数 × 2指数で表されます。仮数は、1.bbb...の形式の64ビットの2進数です。つまり、先頭の1、その後に2進小数点(10進小数点の2進数相当)、そして残りのビットが続きます。5 浮動小数点数が有用である理由は、指数が-16382から16383の範囲をカバーするため、その範囲は非常に小さい値から天文学的に大きい値まで及ぶことです。重要な詳細として、指数は16383の「バイアス」が加算されて格納されます。したがって、実際の指数が負であっても、格納される指数は常に正です。6

[画像: 80ビット一時実数フォーマット。三角形は2進小数点の位置を示しており、10進小数点に相当します。Intel Numerics Supplementより。]

80ビット一時実数フォーマット。三角形は2進小数点の位置を示しており、10進小数点に相当します。Intel Numerics Supplementより。

8087は、以下に示すように特殊なケースとして表現されるいくつかの種類の数値をサポートしています。ゼロと無限大には正と負の両方の値があります。「非数(Not a Number、NaN)」は、0/0やsqrt(-1)など、意味をなさない値を表します。NaNには単一の値ではなく、多数の表現があります。8087は、仮数に先頭の1がない、非常に小さい値である非正規化数および非正規化数もサポートしています。

[画像: 特殊値のエンコーディング。Intel Numerics Supplementの表S-31に基づき、大幅に簡略化されています。「x」ビットは、他のタイプと競合しない限り任意です。]

特殊値のエンコーディング。Intel Numerics Supplementの表S-31に基づき、大幅に簡略化されています。「x」ビットは、他のタイプと競合しない限り任意です。

8087は、算術演算で問題が発生した場合を示す6種類の例外を持つ複雑な例外システムを備えています。最も深刻なのは「無効な操作」で、0/0や∞-∞など、操作が意味をなさないことを示します。また、空のレジスタへのアクセス(スタックオーバーフローまたはアンダーフロー)やNaN値に対する操作も含まれます。8087には、値が大きすぎて格納できない場合のオーバーフロー例外、値が小さすぎて格納できない場合のアンダーフロー例外、およびゼロ除算例外(0/0を除く)もあります。非正規化オペランド例外は、結果が正規の値として格納するには小さすぎるが、非正規化数として格納できることを示します。最後に、精度例外は、値が正確に表現できず丸める必要があることを示します。(精度例外は非常に一般的です。1/10でさえ1つ生成されます。)

8087は、各例外タイプに対して細かい制御を提供しており、制御レジスタのビットで指定されます。例外がマスク解除されている場合、8087は8086プロセッサに割り込みを送信し、ソフトウェアで問題を処理します。例えば、プログラムを終了したり、エラーをログに記録したりします。あるいは、例外はマスクされ、8087は可能な限り実行を続行します。例えば、無効な結果はNaNに置き換えられ、オーバーフローまたはゼロ除算は無限大に置き換えられます。精度例外は丸め処理の結果となります。(マスクされた例外のポイントは、計算が続行され、可能な限り正確な結果が得られることです。ほとんどの場合、これがプログラマーが望むことです。)

これらの機能により、8087は柔軟で正確なものになりますが、特殊ケースの組み合わせを適切に処理する必要があるため、マイクロコードもはるかに複雑になります。

8087命令の実行には、結果を計算するために数百の内部ステップが必要になる場合があります。これらのステップは、各アルゴリズムステップを指定するマイクロ命令を持つマイクロコードで実装されています。(命令の2つのレベルを覚えておいてください:プログラマーが使用するアセンブリ言語命令と、チップ内の文書化されていない低レベルのマイクロ命令。)マイクロコードROMには、8087の命令セットを実装する1648個のマイクロ命令が格納されています。私はOpcode Collectiveと協力してマイクロ命令をリバースエンジニアリングし、マイクロコードを完全に理解しています(リンク)。

8087のマイクロ命令は複雑で、多くのコーナーケースとアドホックな関数がありますが、簡単な概要を提供します。各マイクロ命令は16ビットで構成されます。最初の3ビットはマイクロ命令のタイプを指定し、残りのビットの意味を制御します。最初のタイプは転送操作で、データを内部レジスタ間で転送します。2つのフィールドがソースとデスティネーションを指定します。残りの3ビットは様々な特殊ケースに使用されます。次に、バレルシフターを使用して値を左または右にシフトするシフト操作があります。3番目のタイプのマイクロ命令は加算器(減算も可能)を制御します。その他の命令には、スタックポインタ操作、タグ変更、例外、およびサブルーチンリターンが含まれます。遠距離ジャンプ(far jump)および遠距離呼び出し(far call)マイクロ命令は、固定リスト内のターゲットマイクロアドレスへのジャンプまたはサブルーチン呼び出しを実行します。条件フィールドにより、多数の条件に基づいて条件付きジャンプ/呼び出し/リターンが可能になり、最後のビットが条件を反転させます。ローカルジャンプは、近くのマイクロ命令への相対ジャンプです。

[画像: 8087マイクロ命令の構造。]

8087マイクロ命令の構造。

FSCALEマイクロコード

8087が命令の実行を開始すると、命令デコーダ回路が命令に対応するマイクロコードの開始アドレスを決定します。この11ビットのアドレスはマイクロコードエンジンにロードされ、マイクロコードの実行が開始されます。7 FSCALEのマイクロコード(以下に示す)は、10進アドレス748から始まります。8

FSCALEの背後にある考え方は単純です。浮動小数点数を2^N(整数N)でスケーリングしたい場合、数値の指数にNを加算します。これにより、完全な浮動小数点乗算操作よりもはるかに高速に乗算または除算できます。しかし、FSCALEのマイクロコードは予想外に複雑で、いくつかのマイクロコードサブルーチンを使用しています。要約すると、マイクロコードはまずゼロの引数をチェックし、次に他の特殊な引数を処理します。スケール引数を整数に変換し、指数に加算します。最後に、オーバーフローまたはアンダーフローを処理します。

詳細には、マイクロコードルーチンはスタックの最上部(st(0))から最初の引数をtmpA一時レジスタに移動させることから始まります。引数がゼロの場合、ルーチンはすぐにリターンします。(したがって、0を(NaNであっても)何でスケーリングしても結果は0になります。)次に、スタック上の2番目の値(2番目の引数)がtmpB一時レジスタに移動されます。同様に、この値が0の場合もコードはリターンするため、何かを0でスケーリングしても値は変更されません。9 次に、定数値が選択されます。定数を選択することとそれを使用することは、2つの別々のマイクロ命令です。(8087には、16ビット指数定数と67ビット仮数定数用の別々のROMがあります。これは指数定数です。)通常のケースでは、実行はアドレス#0763にジャンプし、SPECIAL_TMPSの呼び出しをスキップします。

FSCALE:
#0748 st(0) -> tmpA        スタックからの入力引数
#0749 jmp #0776 if tmpA:tag ZERO 0ならベイル
#0750 stackPtr++
#0751 st(0) -> tmpB        スタック(1)からのスケール引数
#0752 stackPtr--
#0753 jmp #0776 if tmpB:tag ZERO 0ならベイル
#0754 expconst 0x403e      定数 403e: intへの変換用指数シフト
#0755 jmp #0763 if not tmp empty/special/div
#0756 call SPECIAL_TMPS    特殊処理
#0757 jmp #0762 if flag
#0758 jmp #0761 if not tmpB:tag SPECIAL
#0759 except:invalid       無効例外、NaNを使用
#0760 NaN -> tmpA
#0761 jmp #0776 if intr
#0762 jmp #0775 if expConv[0] expConvが設定されていればtmpAをリターン、そうでなければ続行
#0763 tmpB:exp -> Breg     通常パス
#0764 tmpB:sign,exp -> expConv ExpConvがtmpBの符号をテストします
#0765 expConst -> tmpC     定数 403e
#0766 adder: tmpC - Breg cin=1 403e-expはtmpBをintに変換するためのシフト量
#0767 sumreg:frac -> shiftcount シフター制御に格納
#0768 shift tmpB:frac R count byte bit シフトを実行
#0769 shift R -> Breg      Bregがintとしてのスケール引数を保持
#0770 jmp #0777 if neg     負のBregは別途処理が必要
#0771 adder: tmpA:exp + Breg cin=0 スケールを指数に加算
#0772 sumreg:frac -> expConv 結果をexpConvに入れてチェック
#0773 sumreg:frac -> tmpA:exp 指数を合計で更新
#0774 call NONNORMAL_RESULT オーバーフロー/アンダーフローを処理
#0775 tmpA -> st(0)        結果をスタックに保存
#0776 RNI                  完了:次の命令を実行
#0777 adder: tmpA:exp - Breg cin=1 減算
#0778 jmp #0772            処理を続行

#0763から続くと、2番目の引数は数ステップで整数に変換されます。例えば、引数が9(浮動小数点数では1.001×2^3)だとします。仮数ビット1000は「左寄せ」されますが、整数ではこれらのビットを右にシフトして「右寄せ」する必要があります。一般に、指数がnの場合、仮数は63-nビット右にシフトされます。しかし、指数には16383のバイアスがかかっていることを思い出してください。したがって、仮数は63-(exp-16383)ビット、つまり0x403e-expビット右にシフトする必要があります。(これが、マイクロコードの前の定数0x403eの説明です。)

[画像: 浮動小数点数をシフトして整数に変換する様子。]

浮動小数点数をシフトして整数に変換する様子。

マイクロコードでは、減算は数ステップかかります。#0763では、2番目の引数の指数が加算器の一方の入力であるBレジスタに移動されます(tmpBとは全く別物です)。10 次に、符号と指数が指数コンバーターに移動されます。この回路は、とりわけオーバーフローをテストします。次に、定数0x403e(#0754で再度選択されたもの)がtmpCレジスタに移動されます。#0766では、加算器がアクティブになり、指数から定数を減算します。11 加算器は結果をSumレジスタに入れ、この値はシフトカウントレジスタにコピーされ、シフターを制御します。この値は、2番目の引数を整数に変換するためにシフトする必要があるビット数を示します。#0768では、ビットシフト部分とバイトシフト部分の両方を使用して、シフターが所望の量だけシフトするようにアクティブになります。加算器と同様に、シフターをアクティブにして結果を読み取ることは別々のマイクロ命令です。結果はBレジスタに入れられます。

FSCALE命令のコア部分は、最終的に#0771で実行され、2番目の引数を最初の引数の指数に加算します。加算器がアクティブになり、Bレジスタの値(スケール)を指数に加算し、更新された値はtmpAの指数に格納されます。(スケールファクタが負の場合を除き、#0777パスを通じて減算されます。)12 値は指数コンバーター回路にも送られ、指数がオーバーフローまたはアンダーフローしていないかチェックします。もしそうであれば、サブルーチンNONNORMAL_RESULTが呼び出されます。しかし、通常のケースでは、更新された値はtmpAからスタックトップレジスタst(0)にコピーされます。最後に、RNI(Run Next Instruction)は、マイクロコードルーチンが完了し、命令が実行されたことを示します。したがって、単純なケースであっても、FSCALEは約22マイクロ命令を要します。

空または特殊な引数の処理

引数が空のスタック位置(つまりスタックアンダーフロー)にアクセスした場合や、特殊な値(無限大、非正規化数、NaN)であった場合はどうなるでしょうか?これらのケースは、tmpAおよび/またはtmpBの特殊値を処理するため、私がSPECIAL_TMPSと呼ぶマイクロサブルーチンによって処理されます。15 このサブルーチンは、例えば一方の引数が空で他方が非正規化数である場合など、問題を優先的に処理する必要があるため、汎用ルーチンです。基本的な算術演算、FSCALE、FTST(テスト)、およびFPREM(部分剰余)によって使用されます。

SPECIAL_TMPSの制御フローは、コードが問題を優先順位付けする必要があるため、かなり複雑です。ここでは簡単な概要のみを示します。詳細は脚注13を参照してください。まず、サブルーチンは非正規化数を非正規化数に変換します。次に、空のスタック位置へのアクセスをチェックし、そうであれば例外または割り込みを発生させます。次に、2つの引数を再度チェックします。どちらかがNaNの場合、例外または割り込みがトリガーされます。それ以外の場合は、引数のタイプを示すステータスを返します。

予期せぬことに、両方の引数がNaNの場合、コードは2つのNaN値を比較して大きい方を返します。この動作は非常に奇妙に見えるかもしれませんが、文書化された機能です。14 NaNは単一の値だと考えるかもしれませんが、実際には膨大なファミリーです。プログラマーが問題が発生した場所を示すために異なるNaN値を使用できるという考え方でした。例えば、初期化されていない配列の各位置に異なるNaNを配置することで、どの位置にアクセスされたかを特定できます。何らかの理由で、8087の設計者は、2つの異なるNaNで演算を実行した場合、結果は大きい方になると決定しました。したがって、マイクロコードは、両方のオペランドがNaNであることを検出し、大きい方を計算するためのコードを必要とします。比較のために減算を使用します(#1518)。

SPECIAL_TMPS (J5):
#1484 call SPECIAL_VAL if tmpA:tag SPECIAL tmpA/tmpBの特殊値を処理
#1485 xchg tmp
#1486 call SPECIAL_VAL if tmpA:tag SPECIAL tmpBの特殊値を処理
#1487 xchg tmp
#1488 1 -> flag            デフォルトでフラグ=1
#1489 jmp #1500 if not tmp empty/special/div tmpsがOKならexpConvに0をジャンプ
#1490 1 -> expConv
#1491 jmp #1497 if not tmpA/B empty
#1492 except:invalid       どちらかが空なら無効
#1493 jmp #1525 if compare instruction 比較命令にはNaNなし
#1494 jmp #1511 if intr     割り込みがあればリターン
#1495 NaN -> tmpA          割り込みがマスクされていればNaN
#1496 return
#1497 jmp #1502 if tmpA:tag SPECIAL 特殊ケース
#1498 jmp #1505 if tmpB:tag SPECIAL
#1499 0 -> flag            除算通常パス:
#1500 zero -> expConv      フラグ0、expConv 0を返す
#1501 return
#1502 call SPECIAL_VAL     tmpA特殊
#1503 jmp #1512 if not flag NaNならジャンプ、無限大ならフォールスルー
#1504 jmp #1509 if not tmpB:tag SPECIAL
#1505 xchg tmp             tmpB特殊
#1506 call SPECIAL_VAL
#1507 xchg tmp
#1508 jmp #1521 if not flag NaNでなければジャンプ、無限大ならリターン
#1509 0 -> flag            フラグをクリア、リターン
#1510 return
#1511 RNI                  割り込みで命令終了
#1512 jmp #1522 if not tmpB:tag SPECIAL tmpAはNaN、次にtmpBをチェック
#1513 xchg tmp
#1514 call SPECIAL_VAL     tmpBをチェック
#1515 xchg tmp
#1516 jmp #1522 if flag    tmpBがNaNでないならジャンプ
#1517 except:invalid       無効例外
#1518 tmpB:frac -> Breg    両方の引数がNaN、大きい方を見つける
#1519 adder: tmpA:frac - Breg cin=1
#1520 jmp #1522 if adder sign tmpA < tmpBか確認
#1521 tmpB -> tmpA         大きい方を取る
#1522 except:invalid       無効例外
#1523 jmp #1525 if compare instruction 比較命令には割り込みなし
#1524 jmp #1511 if intr    割り込みで命令終了
#1525 1 -> flag            フラグを設定してリターン
#1526 return               J5の終了

このサブルーチンは、1つの引数を処理するヘルパーサブルーチンであるSPECIAL_VAL16を多用しています。ヘルパーは、非正規化された引数を非正規化数に変換し、必要に応じて例外または割り込みを発生させます。また、無限大の入力をフラグ付けします。

#1485でのtmpAとtmpBを交換するマイクロ命令のハードウェアは興味深いです。レジスタ間で物理的に値を移動する代わりに、マイクロ命令はフリップフロップをトグルしてtmpAとtmpBの意味を交換します。つまり、フリップフロップがセットされている場合、tmpAへの参照はtmpBに行き、その逆も同様です。(これはマイクロプロセッサの標準的なトリックです。Intel 8080のXCHG命令は、同様の方法でDEレジスタとHLレジスタを交換します。Z80は、通常のレジスタセットとセカンダリレジスタセットを交換するためにEXおよびEXX命令で同じトリックを使用します。)

[画像: Intel 8087チップは、8080やZ80と同様に、40ピンDIP(デュアルインラインパッケージ)にパッケージされています。この写真はマイクロコードの休憩として掲載しています。]

Intel 8087チップは、8080やZ80と同様に、40ピンDIP(デュアルインラインパッケージ)にパッケージされています。この写真はマイクロコードの休憩として掲載しています。

非正規な結果の処理

非常に大きな数値をさらに大きくスケーリングすると、オーバーフローが発生する可能性があります。非常に小さな数値をさらに小さくスケーリングすると、非正規化数またはアンダーフローになる可能性があります。これにより、オーバーフロー、非正規化、またはアンダーフロー例外が発生し、マスク解除されていれば割り込みが発生します。さらに、8087は4つの丸めモードをサポートしています:最も近い有効な値への丸め、下方向への丸め(-∞へ)、上方向への丸め(+∞へ)、またはゼロ方向への丸め(切り捨て)。丸めモードによっては、オーバーフローの結果は∞または可能な限り大きい浮動小数点数になります。同様に、アンダーフローの結果はゼロまたは可能な限り小さい浮動小数点数になります。また、無限大モード(アフィンまたは射影)によっては、無限大は符号付きまたは符号なしになります。したがって、FSCALEマイクロコードは、結果に対して多くの特殊ケースを処理する必要があります。

tmpAの非正規な結果を処理するサブルーチンを以下に示します。興味深いマイクロ命令の1つは、「オーバーフロー/アンダーフロー例外の更新」で、適切であれば例外をトリガーします。ほとんどの例外では、マイクロ命令が例外をトリガーします(例えば、#0346のexcept:precision)。しかし、オーバーフローおよびアンダーフロー例外では、マイクロコードはタスクをハードウェアに委任します。具体的には、8087の「指数コンバーター」回路が指数を調べて、選択された浮動小数点精度に基づいてオーバーフローまたはアンダーフローが存在するかどうかを確認します。マイクロ命令は、これらの値に基づいてオーバーフローおよびアンダーフローフラグを設定します。このように、指数コンバーターのハードウェアサポートのおかげで、複雑なタスクが単一のマイクロコード命令で実行されます。

NONNORMAL_RESULT (J16):
#0318 return if tmpA:tag ZERO 非正規な結果を処理
#0319 update overflow/underflow exceptions exp convが指示すれば例外をトリガー
#0320 expconst 0x6000      割り込みバイアス定数 0x6000
#0321 jmp #0329 if not intr
#0322 expConst -> Breg     割り込みパス
#0323 jmp #0326 if neg
#0324 adder: tmpA:exp + Breg cin=0 アンダーフローのためのバイアスを加算
#0325 jmp #0327
#0326 adder: tmpA:exp - Breg cin=1 バイアスオーバーフローのための減算
#0327 sumreg:frac -> tmpA:exp tmpAへの新しい指数
#0328 return               割り込みなので完了
#0329 jmp #0344 if neg     マスクされた例外
#0330 tmpA:exp -> Breg     アンダーフロー
#0331 adder: 1 - Breg cin=1 非正規化数のシフト量
#0332 call CREATE_DENORM   非正規化数を作成
#0333 adder: zero + Breg cin=0, roundmode 丸めのためにゼロを加算
#0334 call ADJUST_PRECISION  指定された精度に調整
#0335 jmp #0340 if Sum register is zero ゼロなら、適切に+/-ゼロを返す
#0336 zero -> tmpA:exp     非正規化数:指数は0
#0337 sumreg:frac -> tmpA:frac 非正規化数の仮数を保存
#0338 special -> tmpA tag  非正規化数を特殊としてタグ付け
#0339 return
#0340 tmpA sign -> sign latch +/-ゼロを返す
#0341 zero -> tmpA
#0342 sign latch -> tmpA sign
#0343 return
#0344 NaN/Inf -> tmpA:exp  オーバーフロー:無限大を返す可能性あり
#0345 tmpA:frac -> tmpB:frac tmpAの仮数をtmpBに保存
#0346 except:precision     精度例外を設定
#0347 Inf -> tmpA:frac     仮数に無限大を配置
#0348 special -> tmpA tag  無限大を特殊としてマーク
#0349 return if not round chop 切り捨て丸めでないならリターン
#0350 1 -> Breg            最大フロートを返す:下方向へ調整
#0351 adder: tmpA:exp - Breg cin=1
#0352 sumreg:frac -> tmpA:exp Exp=7fff-1=7ffe
#0353 adder: zero - Breg cin=1
#0354 sumreg:frac -> tmpA:frac Frac 0-1 = ff...ff
#0355 norm -> tmpA tag     正規値
#0356 return if not tmpB:frac[63] 非正規化数でない限り最大フロートを返す
#0357 tmpB:frac -> tmpA:frac 元のtmpAの仮数を返す
#0358 return

オーバーフローまたはアンダーフローがマスク解除され、割り込みが発生した場合の8087の動作は興味深いです。その考え方は、割り込みハンドラに指数がどうあるべきだったかを知らせることです。しかし、適切な値は大きすぎるか小さすぎるため指数フィールドに収まらない(だから例外が発生した)ので、使用できません。解決策は、定数0x6000を加算または減算することです。これにより、指数フィールドに収まる値が得られます。割り込みハンドラは、この定数を減算または加算して正しい指数を取得できます。行#0322から0328は、この加算または減算を実行します。

マスクされたアンダーフローの場合、非正規化数を作成するためにサブルーチンCREATE_DENORMが使用されます。値はADJUST_PRECISIONによって指定された精度に丸められます。最後に、値が非正規化数にも小さすぎる場合、適切に+0または-0が返されます。

マスクされたオーバーフローの場合、8087は指定された丸めモードに応じて、無限大または可能な限り大きいフロートを返します。無限大は、すべての1からなる指数と、1000...の仮数で表されます。これらの値はトランジスタによって直接バスにロードされます。しかし、最大フロートは計算されます。無限大の指数から1が減算され、ゼロの仮数から1が減算されます。

ヘルパーサブルーチン:非正規化数の作成

8087の論争のある機能の1つは、通常のフロートよりも小さい数値である非正規化数です。浮動小数点数は、先頭ビットが1に設定された仮数を持つことを思い出してください。しかし、可能な限り小さい指数に達し、さらに小さい数値が必要な場合はどうなるでしょうか?8087は、仮数が1で始まるという規則を破ることを許可し、非正規化数または非正規化数と呼ばれるより小さい数値を生成します。非正規化数は範囲を大幅に拡張し、最大で2^63倍小さい数値を可能にします。しかし、非正規化数は、上位ビットが「無駄」になるため、精度が低くなります。さらに、非正規化数との計算は、特別な処理が必要なため、大幅に遅くなる可能性があります。

[画像: 通常の数値を8倍に縮小して非正規化数になった例。]

通常の数値を8倍に縮小して非正規化数になった例。

上記の図は、最小可能な指数(バイアス後で1に相当する-16382)を持つ通常の数値を示しています。この数値を8で割る(または-3でスケーリングする)と、指数がそれ以上減らせないため、非正規化数が生成されます。代わりに、仮数が右に3ビットシフトされます。指数は非正規化数を示す特別な値0に置き換えられます。

8087では、非正規化数は私がCREATE_DENORMと呼ぶマイクロコードサブルーチンによって作成されます。これはFSCALEだけでなく、多くの算術演算によって使用されます。このサブルーチンは、通常の数値とシフト量を受け取ります。通常の数値をシフトすること(上記の例のように)で、非正規化数を生成します。マイクロコード(以下)は、指数コンバーターを使用して、シフト量が64以上かどうかをチェックします。もしそうであれば、シフト後に何も残らないため、ゼロが返されます。それ以外の場合、値は右にシフトされ、非正規化数はBレジスタに格納されます。

CREATE_DENORM (J20):
#0522 sumreg:frac -> expConv 非正規化数を作成
#0523 sumreg:frac -> shiftcount シフトするビット数
#0524 jmp #0528 if exponent[6:14] == 0 64未満ならジャンプ
#0525 zero -> Breg         ビットが残っていない、ゼロを使用
#0526 shift tmpA:frac L 0 bytes, 0 bits シフターに通す?
#0527 jmp #0532
#0528 shift tmpA:frac R count byte bit 指定された量だけ右シフト
#0529 shift R -> Breg      Bregに結果
#0530 shift tmpA:frac L ~count byte bit スティッキーテストのために再度左シフト
#0531 NOP                  シフターを待つ
#0532 rounding(h) -> Breg[grs] 3つの丸めビットをBregに格納
#0533 return

しかし、なぜ値は左にシフトされるのでしょうか(#0530)?その目的は丸めビットを取得することです。8087の原則の1つは、丸めを正しく行うことであり、これは見かけよりもはるかに困難です。数値を丸めるために、信じられないほど多くのビットを追跡する必要があります。例えば、1 + 0を計算して切り上げると、1になります。しかし、例えば1 + 2^-10000を計算して切り上げると、1よりわずかに高いフロートになります。問題は、数千ビットを格納せずに、丸める前に2つの合計を区別するにはどうすればよいかということです。

トリックは、8087が丸めに使用する3ビット、「ガード」ビット、「ラウンド」ビット、「スティッキー」ビットを保持していることです。仮数の右側にある「テール」ビットを考慮すると、ガードビットはそのテールの最上位ビットであり、その後にラウンドビットが続きます。スティッキービットは特別です。それはテールの残りのすべてのビットのORであり、いずれかが1であるかどうかを示します。したがって、1 + 2^-10000はスティッキービットがセットされますが、1 + 0はセットされないため、2つの値は異なる方法で切り上げることができます。スティッキービットを生成するために、8087はテールビットを並列にテストする非常に大きな64ビットNORゲートを使用します。

[画像: ガード、ラウンド、スティッキービットが右シフトからどのように計算されるかを示す図。この例の数値は前の例とは異なります。]

ガード、ラウンド、スティッキービットが右シフトからどのように計算されるかを示す図。この例の数値は前の例とは異なります。

数値が右にシフトされると(例えば、非正規化数を作成するとき)、ビットが右から失われます。丸めビットを生成するために、値は左にシフトされ、最終的に破棄されるすべてのテールビットを保持し、最終的な仮数に含まれるビットを破棄します。上位2ビットはガードビットとラウンドビットになり、残りのビットはすべてORされてスティッキービットが生成されます。17 上の図はこのプロセスの例です。値が4ビット右にシフトされていると仮定します。テールビットabcd(少なくともd)はシフトで失われます。丸めビットは、元の仮数を59ビット(4の補数)右にシフトして計算されます。ビット62(a)が新しいガードビットになり、ビット61(b)が新しいラウンドビットになり、残りの64ビットのORが新しいスティッキービットになります。(古いガード、ラウンド、スティッキービットもORされているため、失われません。)最初のシフトからの仮数と2番目のシフトからの丸めビットをマージすると、目的の結果が得られます。

ヘルパーサブルーチン:精度の調整

8087は3つの長さのフロートをサポートしていますが、すべての計算を80ビットの「一時実数」で行います。命令の最後に、結果を指定された長さに変換します。(その結果、短いフロートを使用してもほとんどの命令は速くなりません。)私がADJUST_PRECISIONと呼ぶマイクロコードサブルーチンは、8087の制御ワードで指定された精度に結果を変換し、指定された丸めモードを使用します。このサブルーチンは、ほとんどの算術命令で使用されます。

[画像: 8087は3種類の実数をサポートしています。Intel Numerics Supplementより。]

最初のコードパスは一時実数(64ビットの精度を持つ)を処理します。制御ワードは4つの丸めモードのいずれかを指定します。しかし、特定の仮数に対して実行できるアクションは2つだけです。切り捨て(chop)または切り上げ(chopして1を加算)です。この決定は、丸めビット、丸めモード、および符号を調べて切り上げまたは切り下げを決定する複雑な論理回路によって行われます。これによりマイクロコードは簡略化されますが、ハードウェアはより複雑になります。マイクロコードは条件付きリターンを実行し、仮数を切り上げる必要がない場合はリターンします。それ以外の場合、マイクロコードは仮数に1を加算してインクリメントします。次にオーバーフローをチェックし、その場合は値を無限大に置き換えて特殊フラグを設定します。18

ADJUST_PRECISION (J11):
#0299 jmp #0306 if not precision64
#0300 return if not round up, update CC1 条件コードを更新、リターンする可能性あり
#0301 adder: sumreg:frac + 0 cin=1 切り上げのために1を加算
#0302 return if not sumreg[64]
#0303 Inf -> sumreg:frac,sign オーバーフローの場合無限大を返す
#0304 2count++             特殊フラグを設定
#0305 return
#0306 23/52 -> shiftcount  ショートまたはロングリアル:適切なシフトを取得
#0307 shift sumreg:frac,rnd L count byte bit sticky 丸めビットを生成するためにシフト
#0308 NOP                  シフター完了を待つ
#0309 rounding(H) -> sumreg[grs] 丸めビットを格納
#0310 shift sumreg:frac R ~count byte bit 余分なビットをドロップするために右シフト
#0311 shift R -> sumreg:frac
#0312 jmp #0314 if not round up, update CC1 必要に応じて切り上げ、条件コードを更新
#0313 adder: sumreg:frac + 0 cin=1 必要に応じて切り上げ
#0314 shift sumreg:frac L ~count byte bit 再配置のために左シフト
#0315 shift L -> sumreg:frac,sign
#0316 return if not sumreg[64] オーバーフローでないならリターン
#0317 jmp #0303            無限大を返す

短い精度(ショートリアルまたはロングリアル)を返す場合、仮数を短縮する必要があるため、コードはより複雑になります。まず、#0306のコードは、制御ワードで指定された精度に応じて、シフターに23または52をロードし、値を左にシフトします。これにより、前のセクションと同様に丸めビットが生成されます。次に、値は右にシフトされ、目的の長さに短縮されます。以前と同様に、切り上げが必要かどうかに応じて、仮数はインクリメントされるかどうかが決まります。最後に、値は左にシフトされ、仮数の最上位ビットが左に配置されます。以前と同様に、切り上げによってオーバーフローが発生した場合、無限大が返されます。

奇妙な機能として、「切り上げ」条件付きジャンプは、8087のプログラマー可視条件コードレジスタ(CC1)を更新するという副作用も持ち、結果が切り上げられたか切り下げられたかを示します。つまり、8087にはこの特定の条件を検出して値を条件コードラッチにロードするための追加回路があります。奇妙なことに、8087のドキュメントはこの条件コードアクションを説明していません。Intelは1987年の387SX浮動小数点チップまでこれを文書化しませんでした。19

結論

浮動小数点演算には、8087以前にも長い歴史があります。例えば、IBM System/360メインフレーム(1964年)は32ビットおよび64ビット浮動小数点数をサポートしていました。1977年、AMDはAm9511浮動小数点チップを導入し、16ビットおよび32ビット浮動小数点数と超越関数をサポートしました。8087を革新的にしたのは、数値専門家のWilliam Kahanのおかげで、数学的に可能な限り正確になるように慎重に設計されたことです。(8087は、今日のほぼすべてのコンピュータで使用され、互換性のない浮動小数点標準のアナーキーを終わらせたIEEE 754標準につながりました。)

8087は、3つの異なるサイズの浮動小数点数、4つのサイズの整数、4つの丸めモード、無限大モード、マスクまたはマスク解除可能な例外のコレクション、非正規化数と非正規化数、符号付きおよび符号なしの無限大、符号付きゼロ、そして非数(NaN)のファミリー全体を持つ、非常に複雑なものになりました。これらの機能が組み合わさることで、多くのコーナーケースが生じます。8087は、特殊回路と複雑なマイクロコードの両方を通じて、この複雑性に対処します。

8087はどれほど複雑なのでしょうか?8087チップを持たないユーザーのために、Intelは8087の命令を完全にエミュレートする(ただしはるかに遅い)8087サポートライブラリを販売しました。エミュレータは16Kバイトの8086コードを必要としましたが、これは完全なBASICインタプリタが8Kに収まる時代には多大な量でした。別の見方をすれば、8087のハードウェアは必要なソフトウェア量を劇的に削減したということです。8087自体は3.3Kのマイクロコードを使用しましたが、8086コードのエミュレータは16Kでした。

私は8087マイクロコードのリバースエンジニアリングを続ける予定です。最新情報は、Bluesky(@righto.com)、Mastodon(@kenshirriff@oldbytes.space)、またはRSSでフォローしてください。私は「Opcode Collective」のメンバー、特にROMイメージをマイクロコードデータに変換し、内容を広範に分析したSmartest BlobとGloriouscowと協力してきました。詳細はGitHubの8087リポジトリをご覧ください。

ノートと参考文献

  1. 8087特許にはハードウェアに関するいくつかの詳細が含まれていますが、残念ながらマイクロコードはありません。以下の特許図は8087のアーキテクチャを示しており、関連部分をハイライトしました。フラクショントバスと指数バスは赤で示されています。加算器と関連レジスタは黄色です。(減算の場合、Bレジスタセレクタは補数を選択します。)シフターは緑です。指数定数ROMと指数コンバーターはオレンジです。一時レジスタとスタックレジスタは青です。

    [画像: 8087のアーキテクチャ。特許に基づく。この画像(または他の画像)をクリックすると拡大表示されます。]

    8087のアーキテクチャ。特許に基づく。この画像(または他の画像)をクリックすると拡大表示されます。

     ↩

  2. 指数コンバーターは驚くほど複雑です。なぜなら、8087には3つの異なるサイズの指数フィールド(8ビット、11ビット、15ビット)を持つ3つの異なる浮動小数点数フォーマットがあるからです。さらに、異なるサイズの指数は異なるバイアスで格納されます。したがって、異なるサイズの指数間での変換は単純ではありません。指数コンバーターは、異なる指数サイズに対するオーバーフローとアンダーフロー、および無限大やNaNなどの特殊値も認識します。指数コンバーターについては、後でさらに詳しく説明する予定です。 ↩

  3. 8087の仮数は名目上64ビット幅です。しかし、8087は丸めのために3つの追加の低位ビット(ガード、ラウンド、スティッキーと呼ばれる)を使用します。これらのビットは、値が常に正しい方向に丸められることを保証します。データパスの一部には、符号またはオーバーフローのための追加ビットがあります。シフターは68ビット幅で、加算器は69ビット幅です。ほとんどの場合、これらの追加ビットは無視し、データパスを64ビット幅と呼びます。 ↩

  4. タグは通常プログラマーには見えませんが、特別な操作を通じてアクセスできます。具体的には、プログラマーは8087の状態をメモリにダンプでき、タグは16ビットの「タグワード」に格納されます。 ↩

  5. 浮動小数点数の外部表現には、暗黙の先頭の1があり、小数点の後のビットのみが明示的に格納されます。これにより、「無料」で1ビットの解像度が追加されます。しかし、内部の80ビット表現には、計算を簡略化するために明示的な先頭の1があります。 ↩

  6. 指数がバイアスされている理由の1つは、2つの浮動小数点数のうち大きい方を見つけるために、指数を個別に調べる必要なく、符号付き整数として辞書順に比較できることです。 ↩

  7. 8087の命令のほとんどはマイクロコードで実装されていますが、一部はハードワイヤードです。命令デコーディングの詳細については、Intel 8087浮動小数点チップの命令デコーディングを参照してください。 ↩

  8. マイクロコードには10進アドレスを使用しています。これは、Opcode Collectiveが10進アドレスを使い始めたためであり、今変更するのは混乱を招くためです。 ↩

  9. マイクロコードは、0を何でスケーリングしても、または何かを0でスケーリングしても、値が変更されないことを示しています。私の見解では、設計者はここでショートカットをしたと考えられます。8087は0×∞をNaNと定義しているため、0×2∞もNaNであるべきであり、FSCALE(0, ∞)は0ではなくNaNになるはずです。設計者は、おそらく、誰もが曖昧なFSCALE命令のコーナーケースを気にしていないという妥当な判断を下しました。他の命令では、非正規化数、非正規化数、およびゼロでの動作は文書化されています(Numerics Supplementドキュメントの表S-24からS-26)が、FSCALEは省略されています。 ↩

  10. 8087には指数と仮数のための別々のバスがあり、加算器は仮数バスにのみ接続されていますが、指数はどうやって加算器に到達するのでしょうか?トリックは、指数バスと仮数バスの間に16ビットのゲートウェイがあり、指数をコピーできることです。 ↩

  11. 8087の加算器についてはこちらで説明しました。要するに、減算は、Bレジスタの値が加算器に供給されるときに反転させることによって実行されます。加算器へのキャリーインは1に設定されているため、これは実質的に2の補数減算を実行します。 ↩

  12. マイクロコードには、正のスケールを加算するパスと負のスケールを減算するパスが別々にあるのはなぜでしょうか?その理由は、値は符号ビットと符号なしの値として格納され、標準整数のような2の補数ではないからです。その結果、加算器は直接符号付き加算を実行できません。代わりに、加算器回路は明示的にBレジスタの値を補数にして減算を実行するように指示する必要があります。 ↩

  13. このフローチャートはSPECIAL_TMPSサブルーチンを示しています。このルーチンの構造は、パスが分岐して再結合するため複雑です。注意が必要なパスの1つは、NaNが0、1、または2個あるかどうかを判断し、2個ある場合は最大NaNを取るコードです。もう1つの複雑さは例外終了パスです。これは、割り込みがマスクされていない場合は割り込みを発生させますが、比較命令の場合は発生させません。2つの戻り値はflagとexpConvを通じて返されます。

    [画像: SPECIAL_TMPSサブルーチンのフローチャート。クリックすると大きなバージョンが表示されます。]

    SPECIAL_TMPSサブルーチンのフローチャート。クリックすると大きなバージョンが表示されます。

    SPECIAL_TMPSの動作は以下の通りです。flagフリップフロップと指数コンバーターレジスタ(expConv)を通じてステータスを返します。その動作は以下の通りです。

    入力結果flagexpConv
    emptyNaN, exception11
    NaN(larger) NaN, exception11
    infinityinfinity01
    denormunnorm10
    div abnormalno change00

    (最後の行は除算計算中の異常な値を信号します。まだ調査中です。) ↩

  14. 8087の開発を指導したWilliam Kahan教授は、一部の浮動小数点機能が使用されなかったことを残念に思っていました。これは悪循環のためでした。機能は優れたコンパイラサポートを受けられず、プログラマーは機能を使用せず、コンパイラ開発者は需要がないと主張してサポートを実装しませんでした。NaNの複数の値を使用してNaNがどのように、またはどこで発生したかを記録することは、ソフトウェアサポートが不足していた機能の例でした。NaNおよびその他の問題の詳細については、IEEE Standard 754 for Binary Floating-Point Arithmeticのステータスに関する講義ノートを参照してください。 ↩

  15. 8087はマイクロサブルーチンを多用しており、マイクロコードサブルーチン呼び出し用に6レベルのスタックがあります。マイクロコードジャンプとサブルーチン呼び出しは、ジャンプテーブルからアドレスを取得します。ジャンプテーブルからのインデックスは、マイクロ命令の6ビットから取得されます。私たちは無味乾燥にマイクロコードジャンプテーブルのエントリをインデックスに基づいてJ0、J1などと名付けましたが、理解するにつれてより意味のある名前を追加しています。マイクロ命令の名前については、Intelがどのような名前を使用していたかについての情報はありません(8086とは異なり)。Gloriouscowの逆アセンブリの名前から影響を受けて、名前を考案しました。 ↩

  16. 私がSPECIAL_VALと呼ぶサブルーチンは、非正規化数、無限大、およびNaNを処理します。(このサブルーチンは主にSPECIAL_TMPSによって使用されますが、FRNDINT(整数への丸め)およびFSQRT(平方根)によっても使用されます。)まず、サブルーチンはtmpAの指数を見ます。指数がゼロの場合、値は非正規化数です。(値がゼロである可能性もありますが、それは以前に処理されました。)もしそうであれば、denorm例外が設定されます。比較命令(例:FCOM)は非正規化数を異なる方法で処理しますが、ここでは無視します。#1576のコードは、denormが割り込みをトリガーしたかどうかをテストします。もしそうであれば、命令は割り込みで終了します。割り込みがマスクされていた場合、コードはタグをnormに変更し、指数を1に変更して(指数バイアスのために非常に小さい有効な値に対応)、非正規化数を非正規化数に変換します。サブルーチンの結果は、特別なフラグフリップフロップを通じて返されます。

    SPECIAL_VAL (J12):
    #1572 tmpA:exp -> sumreg:frac 特殊値を処理
    #1573 jmp #1581 if not Sum register is zero 指数で非正規化数をテスト
    #1574 except:denorm
    #1575 jmp #1577 if compare instruction 比較命令に例外なし
    #1576 jmp #1571 if DE (denormalized) interrupt RNI(例外の場合)
    #1577 norm -> tmpA tag     非正規化数を処理:タグは空か?それとも有効か?
    #1578 1 -> tmpA:exp        非正規化数に変換
    #1579 0 -> flag            フラグをクリア
    #1580 return
    #1581 shift tmpA:frac L 0 bytes, 1 bits 無限大とNaNをチェックするためにシフト
    #1582 shift L -> sumreg:frac
    #1583 jmp #1579 if not Sum register is zero NaNのためにフラグをクリア
    #1584 1 -> flag            無限大のためにフラグを設定
    #1585 return
    

#1581では、コードは値が無限大かNaNかをチェックします。興味深いことに、このテストは直接行われるのではなく、値をシフターで操作することによって行われます。無限大は仮数10...00を持ち、NaNは少なくとも1つの追加の1ビットを持つことを思い出してください。コードは仮数を1ビット左にシフトします。ゼロの結果は無限大を示し、非ゼロの結果はNaNを示します。以前と同様に、結果はflagフリップフロップで返されます。 ↩

  1. 丸めビットを計算するロジックは、説明したよりも複雑です。expConvの値に応じてわずかに異なる動作をする2つのマイクロ命令がありますが、ここでは触れません。 ↩

  2. ADJUST_PRECISIONサブルーチンは、切り上げ後に仮数がオーバーフローした場合に無限大を返すように見えますが、これには完全には満足していません。例えば、1.111...は無限大ではなく2に丸められるべきです。仮数はオーバーフローしますが、フロートのオーバーフローではありません。おそらく、これはどこかで修正されるでしょう。 ↩

  3. Intelが、条件コードが値が切り上げられたか切り下げられたかを示す機能文書化しなかった理由は不明です。8087のドキュメントはコーナーケースについて非常に詳細であり、通常、奇妙な回路を見つけた場合、それがなぜそこにあるのかを説明するドキュメントの行を見つけることができます。おそらく、条件コード機能はバグがあったため、文書化しない方が簡単だったのでしょうか?この機能は競合他社がチップをコピーするのを捕らえるための隠しトラップだったのでしょうか?(Intelは8086にこの目的のための秘密命令を持っていましたが、NECの8086バージョンにはそれがなく、Intelの弁護士を失望させました。)おそらくIntelは、後続バージョンでこの機能をサポートするかどうか確信が持てなかったのでしょうか?(これが、8085プロセッサの一部の命令が文書化されていない理由です。)今のところ、謎です。 ↩