decimal の設計
このページでは Luna-Flow/floating/decimal の算術モデルを説明します。10 進浮動小数点数とは何か、コホートと推奨指数がどのように情報を運ぶか、交換形式のエンコーディングが数字をどのようにビットに詰め込むか、すべての結果がどのようにちょうど 1 回だけ丸められるか、そこからどのような誤差限界が従うか、指数範囲がどのように強制されるか、そして初等関数がどのように認証されるか、です。各関数の仕様は decimal API に、使用例は decimal チュートリアルにあります。
設計目標
decimal は IEEE 754-201911 IEEE Std 754-2019, Standard for Floating-Point Arithmetic、3.3–3.5 節(10 進形式とエンコーディング)、4 節(属性と丸め)、5 節(演算)、7 節(例外)、9 節(推奨演算)。M. F. Cowlishaw による General Decimal Arithmetic 仕様(バージョン 1.70)は同じモデルを任意精度の形で与えています。本ページではその用語 coefficient(係数)、adjusted exponent(調整指数)、Etiny、clamp を使います。 の 10 進算術を任意の精度で実装しており、次の 3 つの性質を持ちます。
- すべてのコンテキスト演算は正しく丸められます。 結果は、厳密な数学的結果を、選択された方向に、コンテキストの精度と指数範囲へ 1 回だけ丸めたものです。これは基本演算でも初等関数でも同じです。
- 何も黙って失われません。 結果の指数(その量子)、ゼロの符号、NaN のペイロード、およびすべての例外条件は、返される値または返される
DecimalFlagsの一部になります。 - 隠れた状態はありません。 精度、丸め、指数範囲、フラグは、Luna-Flow のどこでもそうであるように、明示的に渡され返される通常の不変な値です。
数学的背景
10 進浮動小数点数
精度 、調整指数の範囲 の 10 進浮動小数点形式は、次の数の集合です。
これに と NaN を加えます。ここで は係数、 は指数または量子であり、
ゼロでない の調整指数は です。これは を科学的記数法 で書いたときの指数です。ゼロでない は のとき正規化数、そうでないとき非正規化数です。最小の正の非正規化数は 、最大の有限値は次のとおりです。
DecimalContext が保持するのは、ちょうど 、、、丸めモード、clamp( を超える指数を許すかどうか。クランプを参照)、および極小性(tininess)の規則です。交換形式は次のとおりです。
| 形式 | バイアス | 指数の個数 | |||||
|---|---|---|---|---|---|---|---|
| decimal32 | 7 | 96 | 90 | 101 | |||
| decimal64 | 16 | 384 | 369 | 398 | |||
| decimal128 | 34 | 6144 | 6111 | 6176 |
IEEE 754 は と定めているので、指数の個数は です。標準は を選んでこの個数を にしています。これはちょうど、値 をとる先頭 2 ビットの指数ビットと、さらに ビットで符号化できる個数です(エンコーディングを参照)。 を非負の格納指数に変換するバイアスは で、decimal64 では です。
なぜ 10 進か
既約分数で表した有理数 が基数 で有限展開を持つのは、 のすべての素因数が を割り切るとき、かつそのときに限ります。 で許される分母は 2 のべきだけで、 では です。したがって、すべての 2 進浮動小数点数は有限の 10 進展開を持ちますが、 は 2 進では有限展開を持ちません。これに最も近い Double は次のとおりです。
したがって、10 進で定義される量(価格、利率、計測値、プロトコルのフィールド)は 10 進浮動小数点で厳密に表現され、10 進の丸めは人や規則が指定する小数位で行われます。その代償は、より大きな wobble(後述)と、より高価な数字の算術です。
コホートと量子
写像 は単射ではありません。同じゼロでない値のすべての表現は、その値のコホートをなします。 が 桁で末尾に 個のゼロを持つとき、指数範囲を無視すれば、メンバーは に対する であり、コホートは 個のメンバーを持ちます。たとえば decimal32 における (、、、)は、 の 7 つのメンバーを持ちます。ゼロはすべての指数に対してメンバーを持ちます。
コホートのメンバーは、数値的な等価性では表せない情報を運びます。12.30 は小数 2 桁を、1.2E+3 は有効数字 2 桁を表します。そのため IEEE 754 は各演算について推奨指数を定めており、厳密な結果は、指数がそれに最も近いメンバーで返されます。推奨指数は、厳密な結果が自然に属する位置から決まります。
和と積では括弧内の係数が整数なので、厳密な係数が 桁に収まる限り推奨指数が達成されます:1.20 + 3.40 = 4.60、1.25 × 2.50 = 3.1250。商では、 が有限の 10 進展開を持つ場合にのみ厳密な結果が存在し、そのとき 桁が許す範囲で に向けて移動されるので、2.400 / 1.2 = 2.00 となります。厳密でない結果は常に 桁すべてを使い、これは指数が最小のメンバーです。quantize は指数を明示的な引数とし、reduce_ctx/normalized は指数が最大のメンバーを選びます。
///|
test "design: preferred exponents" {
let ctx = @decimal.DecimalContext::decimal64()
let d = fn(s : String) { @decimal.Decimal::from_string(s).unwrap() }
inspect(d("1.20").add_ctx(d("3.40"), ctx).0, content="4.60")
inspect(d("1.25").mul_ctx(d("2.50"), ctx).0, content="3.1250")
inspect(d("2.400").div_ctx(d("1.2"), ctx).0, content="2.00")
inspect(d("0.0400").sqrt_ctx(ctx).0, content="0.20")
inspect(d("1.5").fma_ctx(d("2.0"), d("0.25"), ctx).0, content="3.25")
}
丸めの方向
を厳密な値とし、目標指数を ( 桁を残す指数、または極小な結果では )とします。次のように書きます。
各丸め方向は または (に を掛けたもの)を返します。どちらを選ぶかは 、 の最後の桁、および符号に依存します。
| モード | IEEE での名前 | を返す条件( のとき) |
|---|---|---|
Down | roundTowardZero | しない |
Up | — | 常に |
Ceiling | roundTowardPositive | |
Floor | roundTowardNegative | |
HalfUp | roundTiesToAway | |
HalfDown | — | |
HalfEven | roundTiesToEven | 、または かつ が奇数 |
ZeroFiveUp | — |
負の に対しては、同じ規則を に適用してから符号を戻すので、Ceiling と Floor が入れ替わります。すべてのモード は単調です:。初等関数の認証が依拠しているのはこの単調性です。
誤差モデル
を正規化数の範囲にあるとし、 とします。この 10 進の桁区間(decade)における表現可能な数は、最終桁の 1 単位 の間隔で並んでいます。最近接への丸めの誤差はその半分以下なので、
この限界は decade の下端付近で達成されます。上端付近 では、同じ絶対誤差は相対誤差にしてわずか です。したがって、1 つの decade 内での最悪と最良の相対誤差の比は次のとおりです。
これが基数 の wobble です。22 Goldberg, “What every computer scientist should know about floating-point arithmetic”, ACM Computing Surveys 23(1), 1991, §1.2; Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.1–2.2. 2 進では wobble は 2 なので、同じ記憶容量では 10 進の方が最悪ケースの相対誤差がわずかに大きくなります。decimal64 は 、binary64 は です。方向付き丸めのモードでは です。マシンイプシロン、すなわち 1 から次に大きい数までの距離は です。epsilon_contextual はこれを返し、decimal64 での next_plus(1) は 1.000000000000001 です。1 未満では間隔が 10 分の 1 になります:next_minus(1) は 0.9999999999999999 です。
非正規化数の結果では間隔が固定の なので、誤差限界は絶対誤差 になります。すべてのコンテキスト演算は正しく丸められるので、標準モデル 、 は、、fma、および結果が正規化数であるすべての初等関数について成り立ちます。したがって Higham の古典的な前進誤差解析と後退誤差解析が でそのまま適用できます。
設計上の判断
1 つの表現、多数のコンテキスト
問題。 アプリケーションは、型の間で変換することなく、decimal32/64/128 と任意精度、IEEE の意味論と GDA 互換性を必要とします。
選択肢。 形式ごとに 1 つの型(ハードウェアのように)。形式をコンテキストで運ぶ 1 つの任意精度型。形式でパラメータ化された型。
選択。 符号、任意長の係数、指数、クラス、NaN の種類を示すビット、作業精度フィールドを保持する 1 つの Decimal 型と、別個の不変な DecimalContext。decimal64 の計算は DecimalContext::decimal64() のもとでの計算であり、交換形式のエンコーダはエンコードの前に形式のコンテキストを適用します。これにより算術が 1 か所にまとまり、呼び出し側は 50 桁で作業して最後に decimal64 へ丸めることができ、General Decimal Arithmetic のモデルとも一致します。値の中の精度フィールドは、コンテキストを持たない演算子と変換のためだけに使われます。
1 か所で 1 回だけ丸める
問題。 二重丸め(すでに丸められた値を再び丸めること)は、正しく丸められた結果を変えてしまうことがあります。
選択。 すべての有限なコンテキスト結果は 1 つの最終化ルーチンを通ります。このルーチンは厳密な結果 ( は よりはるかに長いこともあります)を受け取り、 桁への丸め、オーバーフローの検査、非正規化数のグリッドへの丸め、非正規化数/アンダーフローのフラグ設定、フォールドダウンをこの順に行います。演算のカーネルは厳密な整数を計算するだけで、丸めは一切行いません。例外は厳密な結果が無限桁になる演算(除算、平方根、初等関数)で、これらについては後述します。いずれも最後の桁は厳密な情報から決定します。
丸め桁とスティッキー情報の求め方
( 桁の非負整数)を 桁に丸めるには、10 のべきで割ります。
そして と を比較して と のどちらにするかを決めます。この 1 回の比較が、古典的な丸め桁とスティッキービットの情報をちょうど担っています。丸め桁 と残り を用いて と書くと、
そして なので、
したがって 、、 はそれぞれ「中点より上、中点ちょうど、中点より下」を意味し、half 系のモードに必要なのはこれだけです。 は方向付き丸めのモードが必要とするスティッキー情報であり、ZeroFiveUp と HalfEven に必要なのは の最後の桁だけです。 のときは係数全体が捨てられて となり、中点に達しうるのは の場合だけです。このときコードは と を比較します。 を に変える繰り上がりは、10 による 1 回の厳密な除算と指数のインクリメントで取り除かれます。
rounded は のとき常に、inexact は のとき常に立てられます。丸めの前に末尾のゼロを落とすので、ゼロを捨てるだけなら rounded だけが立ちます。
除算
2 つの有限でゼロでない値の商 は、次の 3 つの厳密な経路のいずれかを、この順に試します。
- 除数が 10 のべきの場合。 商は指数をずらした です。
- 有限小数になる商。 を で約分して とします。商が有限の 10 進展開を持つのは のとき、かつそのときに限ります。 とすると、 したがって厳密な結果は指数 の整数 であり、他の厳密な結果と同様に最終化ルーチンに渡されます。
- 有限小数にならない商。 結果は必ず厳密ではありません。 とします。これは と、適切な 10 のべきでスケールした とを比較することで厳密に求まります。分子(または分母)を でスケールすると、整数商がちょうど 桁になります:。インクリメントの判定は と を比較します。これは上と同じ中点テストで、今度は厳密な剰余がスティッキー情報になります。したがって商は 1 回だけ丸められます。
3 番目の経路は、拡張コンテキストで結果が正規化数の場合に使われます。結果が非正規化数の場合やサブセットコンテキストでは、コードは 桁を計算してコンテキストのモードで丸め、さらに精度と へもう一度丸めます。コンテキストを持たない演算子 / も、同じガード付きの方式を HalfEven で使います。最近接への丸めを 2 回行うことは常に正しいとは限りません。1 回目の丸めが 2 回目の丸めの中点にちょうど一致すると、厳密な値ではすでに決まっていたケースを 2 回目の丸めのタイ規則が決めてしまうからです。したがってこの方式は、そのような中点付近の場合を除いてすべての場合に正確です。演算子 / では、たとえば 5 桁での でこれが現れます(0.00018009 ではなく 0.00018008)。結果が正規化数の場合の div_ctx にはこの弱点はありません。
ZeroFiveUp はまさにこのような 2 段階の方式を安全にするために存在します。 をまず ZeroFiveUp で 桁()に丸め、次に任意のモード で 桁に丸めると、結果は に等しくなります。厳密でない ZeroFiveUp の結果は 0 と 5 以外の桁で終わるので、 桁の数にも 桁の中点にもならず、すべての 桁の数と中点に対して と同じ側にあるからです。証明は添付資料にあります。33 decimal の丸めに関する証明には、二重丸めの補題、オーバーフローの表、フォールドダウンの限界、認証の補題、NTT の限界の完全な証明が含まれています。
平方根
目標指数を とし、オペランドを整数 にスケールします(負の分岐で が奇数なら先に 10 を掛けます)。整数平方根 は整数上の Newton 反復で計算します。
この反復は である間は狭義に減少し(相加相乗平均の不等式 により、また となるのは のとき、かつそのときに限るため)、 で停止します。剰余 がスティッキー情報です。中点テストは であり、 は偶数で は奇数なので等号は起こりえません。平方根がちょうど中点になることはないので、HalfEven、HalfUp、HalfDown は平方根では一致します。非正規化数の目標指数で直接丸めることで、極小な平方根の二重丸めを避けています。厳密な平方根は最初に検出され(指数を偶数にした後で約分した係数が完全平方数である場合)、推奨指数 で返されます。
指数範囲
最終化ルーチンは 桁の丸められた係数 と指数 、すなわち指数範囲を無制限として丸めた値に対して処理を行います。
オーバーフロー
であれば結果はオーバーフローします。IEEE 754 §7.4 は、返される結果を、同じ で指数が無制限の形式で厳密な値を丸め、それを飽和させたものと定義しています。絶対値を決して増やさない方向は有限の範囲を出られず、増やしうる方向は無限大になります。したがって とすると、
| モード | ||
|---|---|---|
HalfEven, HalfUp, HalfDown, Up | ||
Down | ||
Ceiling | ||
Floor | ||
ZeroFiveUp |
half 系のモードが無限大になるのは、オーバーフローする厳密な値は少なくとも であり(それより小さいものは有限値に丸められてオーバーフローしません)、これは と次の 10 のべきとの中点以上だからです。ZeroFiveUp が飽和するのは、 の最後の桁が 9 だからです。すべてのオーバーフローは overflow、inexact、rounded を立てます。
///|
test "design: overflow depends on the rounding direction" {
let ctx = @decimal.DecimalContext::decimal64()
let big = @decimal.Decimal::from_string("9E+384").unwrap()
let ten = @decimal.Decimal::from_int(10)
inspect(big.mul_ctx(ten, ctx).0, content="inf")
let down = ctx.with_rounding(@def.RoundingMode::TowardZero)
let (sat, flags) = big.mul_ctx(ten, down)
inspect(sat, content="9.999999999999999E+384")
inspect(flags.overflow && flags.inexact, content="true")
}
非正規化数、極小性、アンダーフロー
厳密な結果が 未満の指数を必要とする場合、それは非正規化数のグリッドに丸められます。シフト量は となり、保持する桁数が 未満の状態で上記の丸め規則が適用されます。結果は、その調整指数が 未満のとき極小です。調整指数は厳密な値で測る(BeforeRounding)か、指数無制限で 桁に丸めた値で測ります(AfterRounding、デフォルト)。2 つの規則が異なるのは、 のすぐ下にあってそれへ切り上げられる値の場合だけです。極小な結果は subnormal を立て、極小かつ厳密でない結果は、IEEE 754 §7.5 がデフォルトの例外処理として要求するとおり underflow も立てます。ゼロに丸められた結果は指数 と clamped を得ます。
クランプ
clamp が有効な場合(すべての交換形式)、エンコーディングには 個の指数を入れる余地しかないため、 を超える指数は表現できません。 でオーバーフローしなかった結果はフォールドダウンされます。係数に を掛け、指数を に設定し、clamped を立てます。これに 桁を超える桁が必要になることはありません。
ここで を用いました。値は変わらず、コホートのメンバーだけが変わります。decimal32 では、1E+96 は 1000000E+90 として格納されます。
///|
test "design: fold-down in decimal32" {
let ctx = @decimal.DecimalContext::decimal32()
let (x, flags) = @decimal.Decimal::from_string_ctx("1E+96", ctx)
inspect(x.coefficient(), content="1000000")
inspect(x.exponent10(), content="90")
inspect(flags.clamped, content="true")
}
ゼロには埋める桁がないので、その指数は単に (clamp なしでは )にクランプされ、変化したときに clamped が立てられます。
戻り値としてのフラグ
問題。 IEEE 754 のステータスフラグは、ハードウェアではスティッキーなプロセス状態です。GDA はさらにトラップを加えます。隠れた状態は、意味論を明示的にするという Luna-Flow の規則と衝突し、並行なコードや合成的なコードを壊れやすくします。
選択。 各コンテキスト演算はそれ自身の DecimalFlags を返します。combine はフィールドごとの OR なので、フラグ集合は単位元 DecimalFlags::new() を持つ可換で冪等なモノイドをなします。パイプライン全体でどのようにグループ化して蓄積しても同じ集合が得られ、これはまさに状態を持たないスティッキーフラグの意味論です。decimal_checked はこの蓄積をパッケージ化し、decimal_gda はスティッキーなステータスとトラップを別のモデルとして実装します。IEEE の 5 つの例外は invalid_operation、division_by_zero、overflow、underflow、inexact に対応し、GDA の条件(rounded、subnormal、clamped、lost_digits、conversion_syntax、division_impossible、division_undefined、invalid_context)はそれらを細分化したものです。
quantize と same-quantum
x.quantize(y) は、指数がちょうど である の値を返します。
結果はその指数で表現可能でなければなりません。すなわち新しい係数は高々 桁で、 は に含まれ、結果の調整指数は を超えてはなりません。そうでなければ演算は無効です。指数こそが契約であるため(セント単位に量子化した金額は小数 2 桁でなければならない)、別の指数で代用することは決してありません。係数を切り上げると桁が増えることがあるため(精度 2 桁で )、桁数の検査は丸めの後に行います。same_quantum は述語 です(2 つの無限大または 2 つの NaN に対しては真)。指数が一致していなければならない値を組み合わせる前に使うべき判定です。
交換エンコーディング
3 つの形式はすべて同じレイアウトを共有します。符号ビット、5 ビットの組み合わせフィールド 、 ビットの指数継続ビット、 ビットの後続仮数です。ここで です。
| 形式 | |||
|---|---|---|---|
| decimal32 | 6 | 2 | 32 |
| decimal64 | 8 | 5 | 64 |
| decimal128 | 12 | 11 | 128 |
バイアス付き指数 は ビットで、その上位 2 ビットは 00、01、10 の値しかとりません。これが上で導いた という個数です。
DPD。 組み合わせフィールドは指数の上位 2 ビットと先頭の桁 を保持します。 で なら、指数ビットは 、 です。 で なら、指数ビットは 、 です。 は無限大、 は NaN で、次のビットが signaling と quiet を区別します。残りの 桁は、densely packed decimal により 個のデクレット(3 桁を 10 ビットで表す)に格納されます。44 M. F. Cowlishaw, “Densely packed decimal encoding”, IEE Proceedings — Computers and Digital Techniques 149(3), 2002. IEEE 754-2019 §3.5.2 がエンコーディングの表を与えています。コードはそれらをブール式として実装しており、適合性コーパスは 1024 個すべてのデクレットを検査します。 3 桁は 1000 通りの値、10 ビットは 1024 通りの符号を持ち、効率は です。0–7 の桁(3 ビット)を小さい桁、8 または 9 の桁(1 ビット)を大きい桁と呼びます。デクレット は、3 桁とも小さい場合に を使い、そのとき桁は 、、 にそのまま格納されます。 は少なくとも 1 つの大きい桁があることを示し、、次いで がそれがどれかを示します。大きい桁の個数で数えると、
そして です。最初の 3 つの場合はちょうど 512、384、96 個の符号を使います。最後の場合は 8 個の値に対して 32 個の符号があります。、、 が 3 桁の下位ビットを運び、、 は無視されるので、24 個の符号は冗長です。3 桁とも大きい各値には 4 つのエンコーディングがあり、そのうち のものが正準です。デコードは 4 つすべてを受け付け、canonical() はそれらを書き換えます。たとえば 125 はデクレット 0010100101 = 0x0A5(3 桁とも小さい)であり、999 は 0011111111 = 0x0FF で、0x1FF、0x2FF、0x3FF とも書けます。
BID。 係数は 2 進整数として格納されます。符号の後の 2 ビットが 11 でなければ、続く ビットがバイアス付き指数、残りの ビットが係数です。そうでなければ指数は 11 の後に続き、係数は に残りの ビットを加えたものです(“100” が暗黙に補われます)。、、 なので、すべての係数が収まります。係数が のエンコーディングは非正準であり、ゼロにデコードされます。
///|
test "design: redundant DPD declets decode and canonicalize" {
let fmt = @decimal.DecimalInterchangeFormat::Decimal64
let canonical = @decimal.DecimalInterchange::from_hex("#22300000000004FF", fmt).unwrap()
let redundant = @decimal.DecimalInterchange::from_hex("#22300000000007FF", fmt).unwrap()
inspect(canonical.to_decimal(), content="19.99")
inspect(redundant.to_decimal(), content="19.99")
inspect(redundant.is_canonical(), content="false")
inspect(redundant.canonical().to_hex(), content="#22300000000004FF")
}
DecimalInterchange は生のビットを保持するので、非正準な入力は呼び出し側が正準化を決めるまでそのまま残ります。算術は常に Decimal 上で行われます。
認証付き初等関数
問題。 超越関数 に対して、ほとんどすべての 10 進数 で は無理数になるため、近似することしかできません。正しく丸められた結果を得るには、丸めを決定できるほど良い近似が必要です(table maker’s dilemma)。
選択肢。 事前の誤差限界を伴う固定精度の評価(高速だが、正しさはその限界がすべての関数と引数で正しいことに依存する)。厳密な誤差限界を伴う Ziv の適応的戦略。55 A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991; J.-M. Muller et al., Handbook of Floating-Point Arithmetic, 2nd ed., Birkhäuser 2018, ch. 12; 包含区間のモデルについては J. van der Hoeven, “Ball arithmetic”, 2009.
選択。 厳密な包含区間に対する Ziv ループ。演算 と入力 について:
- を 2 進の区間に厳密に変換します:、。これは ビットでの
TowardNegativeとTowardPositiveによるto_bin_floatであり、 となります。 ball_floatを使ってその区間上で を評価します。これは入力区間のすべての点について となる区間を返します。- と (2 進有理数なので厳密な 10 進数)を厳密に
Decimalに変換し、両方を目標コンテキストで丸めます。両者が同じ表現(compare_totalで等しい)と同じフラグを与えれば、それを返します。 - そうでなければ と増やして繰り返します。これを最大 12 回行い、その後は認証の失敗を報告します。
丸めは単調なので、この受理判定は健全です。 ならば であり、外側の 2 つが同じ表現なら、中央のものも同じです。フラグも同様に引き継がれます。ただし 自身が表現可能でない場合に限ります。そのとき一方の端点は共通の結果と異なるので両方とも厳密ではなく、ゼロを含まない区間上では overflow、subnormal、underflow は について単調だからです。したがって表現可能な結果はループの前に処理されます(後述)。
初期作業精度は ビットです。ここで は入力の桁数です。10 進 1 桁には ビットが必要なので、 ビットで入力と目標を余裕をもって表現でき、さらに 64 ビットで包含区間による損失を賄います。スケジュールは 1 ステップごとにおよそ 倍で増え、12 ステップ後の予算は約 ビットです。
2 種類の入力は一致判定を決して通過しないので、ループの前に決定されます。
- 厳密な結果。 が表現可能な 10 進数であれば、方向付き丸めのモードでは がいつまでも異なる隣接値に丸められます。コードは厳密なケース(
exp(0)、ln(1)、、sinpi/cospi/tanpiの整数および半整数の引数、exp2/exp10の整数の引数、、整数べき、…)を検出します。ball_floatは 、、 のような厳密な 2 進の結果に対して点区間を返します。どちらでも検出されない厳密な結果(たとえばpower_ctxによる )は認識されず、精緻化の予算をすべて使い切ります。 - 範囲外の結果。 のような包含区間では、端点が異なるフラグで丸められます。 の値はすべて同じようにオーバーフローし、 の非ゼロ値はすべて同じように(モードと符号のみに応じてゼロまたは最小の非正規化数に)丸められるため、遠い端点はそうした代表値に置き換えられます。2 進のテストでは を用いるので、この置き換えは保守的です。
expでは、 であるため はオーバーフローし、同じ評価により は 未満へアンダーフローします。powerは同じ目的で、方向付き丸めの 128 ビット演算により を評価します。
初等関数は、 または が 999,999 を超えるコンテキストを拒否します(invalid_context)。これにより端点の厳密な変換の大きさが抑えられます。
係数カーネル
問題。 10 進の位置での丸めには、10 のべきによる高速な除算と高速な桁数計算が必要です。大きな精度には、2 乗未満の計算量の乗算と除算が必要です。
選択。 パッケージプライベートの DecCoeff は、 未満のインラインの UInt か、基数 のリムからなるリトルエンディアンの配列(先頭にゼロのリムを持たず、桁数をキャッシュする)のどちらかです。基数 は 未満で最大の 10 のべきなので、リム同士の積は 未満になります。9 桁の倍数だけシフトするのはリムの移動で済み、digits10 はリムの個数と最上位リムの桁数から求まります。BigInt が現れるのは公開境界だけです。
乗算のディスパッチ:
| 形状 | アルゴリズム | コスト |
|---|---|---|
| それぞれ 1 リム | インライン | |
| ゼロのリムが多い(ゼロでないリムが ) | 疎な積 | |
| 短い方の長さのブロックに均等分割 | ||
| 小さい | Comba の列方式 / 筆算 | |
| Karatsuba の閾値以上 | Karatsuba | |
| Toom-3 の閾値以上 | Toom-3 | |
| NTT の閾値以上 | 2 素数 NTT |
Comba カーネルは、 のときに限り、 個のリム積の列を UInt64 に累積します。列の和に入ってくる繰り上がりを加えても高々 ですが、19 個の積では を超えうるからです。
NTT は係数を基数 の桁に分割し、素数 と を法として畳み込みます。どちらも 乗根の 1 の原始根を持つので、長さ までの変換が存在します。畳み込みの係数は 未満の桁同士の積を高々 個足したものなので 未満であり、中国剰余定理によって剰余から厳密に復元されます。
これは 、すなわち である限り成り立ち、変換の上限未満では常に真です。基数 の桁では が必要になり、これはどの でも不可能です。NTT がより小さい桁を使うのはこのためです。限界が成り立たない場合、カーネルは Toom-3 にフォールバックします。
除算は、1 リムによる除算、Knuth の Algorithm D、66 D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., §4.3.1 (Algorithm D) and §4.3.3; R. Brent and P. Zimmermann, Modern Computer Arithmetic, Cambridge 2010, §1.3–1.4 and §2.4 (NTT); C. Burnikel and J. Ziegler, “Fast recursive division”, MPI-I-98-1-022, 1998. Burnikel–Ziegler の再帰的除算、または Newton の逆数除算を使います。Newton 法は に対して により を計算します。 とすると となるので、正しいリムの数は 1 ステップごとに倍になります。反復は下から単調に増加しなければなりません。そうならない場合、あるいは最後の商の補正に ステップより多くかかる場合、ルーチンは Burnikel–Ziegler にフォールバックし、それ自体もサポートしない形状では Algorithm D にフォールバックします。
切り替え点はターゲットごとに計測され、ターゲット固有のファイルに保存されています。
| ターゲット | Karatsuba mul/square | Toom-3 | first NTT mul/square | Burnikel–Ziegler | Newton |
|---|---|---|---|---|---|
| native | 96 / 48 | 1,152 | 1,728 / 640 | 2,816 以上 | 無効 |
| LLVM | 96 / 96 | 2,048 | 4,096 / 2,048 | 2,048 | 4,096 |
| Wasm / Wasm-GC / JS | 96 / 96 | 4,096 | 8,192 / 4,096 | 2,048 | 4,096 |
(リムは 9 桁)。native では NTT の閾値は変換長にも依存します。乗算では 1,728、2,816、4,608、7,680、その後 8,192 リム、2 乗では 640、1,040、1,824、3,648、7,296、その後 8,192 リムです。また Burnikel–Ziegler に入る点は、ブロック長が大きくなると 5,120 リム、10,240 リムに移ります。これらはディスパッチの境界であり、コストを変えるだけで結果を変えることはありません。native の Newton 経路は実装・テスト済みですが、native での計測では切り替え点が見られないため無効になっています。
正しさ/不変条件
- 表現。 有限の
Decimalは を持ちます。DecCoeffのリムは正準です(先頭にゼロのリムがなく、桁数が正確)。ゼロの符号は符号ビットに保持され、coefficient()が符号を持つことはありません。 - 1 回だけの丸め。
+、-、×、fma、sqrt、quantize、変換、および(結果が正規化数の場合の)/のすべてのコンテキスト結果は、厳密な結果を 1 回だけ丸めたものです。初等関数の結果は返される場合は常に正しく丸められており、失敗は近似されることなく報告されます。 - 誤差限界。 これらの演算の結果が正規化数であれば、 であり、half 系のモードでは 、方向付き丸めのモードでは です。
- 厳密さが見える。
inexactは返される値が厳密な結果と異なるとき、かつそのときに限り立てられます。roundedは桁が落とされたときに常に立てられます。 - コホートの保存。 収まる厳密な結果は推奨指数で返されます。
quantizeは指数 を返すか、失敗するかのどちらかです。 - フラグ。
combineは結合的、可換、冪等で、単位元はnew()です。 - 順序。
compareは全前順序です(NaN 同士は等しく、すべての数より大きい。)。compare_totalは表現上の全順序であり、NaN でない値についてはcompareを細分化します。 - エンコーディング。 正準なビットをデコードしてからエンコードすると恒等写像になります。形式に収まる値をエンコードしてからデコードすると、コホート、ゼロの符号、NaN のペイロード(DPD)を含めて恒等写像になります。
- 計算量。 比較、加算、シフト、1 リムによる除算はリム数について です。乗算と除算はディスパッチ表に従います。
中点テスト、ZeroFiveUp の二重丸めの補題、オーバーフローの表、フォールドダウンの限界、認証の補題、カーネルの限界の証明は、添付資料にまとめられています。
適合性のページには、有限の証拠(固定された IEEE コーパス、デクレットの網羅的検査、MPFR で認証された初等関数の行、4 つのターゲット)が記録されています。
却下した代替案
- 2 進の
BigInt係数。 10 進の位置での丸めには、毎回の演算で による除算と桁数の計算が必要です。2 進の係数ではどちらも高価ですが、基数 のリムならリムの移動と 1 回の小さな除算で済みます。 - 暗黙のコンテキストとスティッキーフラグ。 明示性と合成可能性のために不採用としました。フラグのモノイドが同じ情報を与えます。
- NaN で中断する
compare。 NaN を含むデータに対して、ソートや汎用のCompareコードが中断してしまいます。現在のcompareは全前順序であり、IEEE の意味論はcompare_checked、compare_ctx、compare_signal_ctx、compare_totalを通じて利用できます。 - 解析的な誤差限界を持つ固定精度の超越関数カーネル。 小さな精度では高速ですが、関数ごと・精度ごとに正しさの証明が必要になります。包含区間のループは構成上正しく、その失敗モードは明示的です。
- IEEE と GDA を 1 つのパッケージにすること。 スティッキーなステータス、トラップ、トラップの優先順位はすべての演算の型を変えてしまいます。これらは
decimal_gdaにあります。 - すべての結果を正規化すること。 コホートを失うと
12.30と12.3が区別できなくなり、量子に依存するプロトコルが壊れます。
境界
decimal は意図的に次のことを行いません。
- スティッキーなステータスやトラップを保持すること(
decimal_checkedまたはdecimal_gdaを使ってください)。 - コンテキストを持たない演算子をコンテキストへ丸めること。
*は厳密で、+と/はオペランドの精度にのみ丸め、いずれも指数範囲を適用しません。 - 初等関数の認証が成功することを保証すること。精緻化の予算(最後のステップでは非常に長い数を扱い、長い時間がかかることがあります)を使い切ると、
CertificationFailure(try_*_ctx)またはinvalid_operationを伴う NaN(*_ctx)を報告します。 - 精度または指数が 999,999 を超えるコンテキストで初等関数を評価すること。
- 2 進との変換を通して NaN のペイロードを保持すること、あるいは値の精度が形式の精度と異なる BID の NaN ペイロードを保持すること。
- 係数の表現、カーネルの選択、閾値を公開すること。
- 適合性のページにある有限の証拠を超えて適合性を主張すること。
Footnotes
-
IEEE Std 754-2019, Standard for Floating-Point Arithmetic、3.3–3.5 節(10 進形式とエンコーディング)、4 節(属性と丸め)、5 節(演算)、7 節(例外)、9 節(推奨演算)。M. F. Cowlishaw による General Decimal Arithmetic 仕様(バージョン 1.70)は同じモデルを任意精度の形で与えています。本ページではその用語 coefficient(係数)、adjusted exponent(調整指数)、Etiny、clamp を使います。 ↩
-
Goldberg, “What every computer scientist should know about floating-point arithmetic”, ACM Computing Surveys 23(1), 1991, §1.2; Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.1–2.2. ↩
-
decimal の丸めに関する証明には、二重丸めの補題、オーバーフローの表、フォールドダウンの限界、認証の補題、NTT の限界の完全な証明が含まれています。 ↩
-
M. F. Cowlishaw, “Densely packed decimal encoding”, IEE Proceedings — Computers and Digital Techniques 149(3), 2002. IEEE 754-2019 §3.5.2 がエンコーディングの表を与えています。コードはそれらをブール式として実装しており、適合性コーパスは 1024 個すべてのデクレットを検査します。 ↩
-
A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991; J.-M. Muller et al., Handbook of Floating-Point Arithmetic, 2nd ed., Birkhäuser 2018, ch. 12; 包含区間のモデルについては J. van der Hoeven, “Ball arithmetic”, 2009. ↩
-
D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., §4.3.1 (Algorithm D) and §4.3.3; R. Brent and P. Zimmermann, Modern Computer Arithmetic, Cambridge 2010, §1.3–1.4 and §2.4 (NTT); C. Burnikel and J. Ziegler, “Fast recursive division”, MPI-I-98-1-022, 1998. ↩