decimal_gda の設計
このページでは、decimal_gda が実装する数学と、パッケージがこのように作られている理由を説明します。使い方はチュートリアルに、すべての名前の一覧は API ページにあります。
設計目標
decimal_gda は、M. F. Cowlishaw による General Decimal Arithmetic Specification(バージョン 1.70)11 M. F. Cowlishaw, General Decimal Arithmetic Specification, version 1.70 (2009), https://speleotrove.com/decimal/decarith.html。テストスイートは同じ著者による dectest コレクションのバージョン 2.62 です。 を純粋な MoonBit の値として実装します。GDA の演算は、数値以上のものを生成するよう規定されています。すなわち結果、条件の集合、コンテキストのスティッキーなステータスの更新、そして場合によっては、定義済みの結果を利用可能なまま制御を移すトラップです。目標は、そのすべてを正確かつ観測可能な形でモデル化することであり、その結果として
- 各演算は、(オペランド、コンテキスト)から(結果、立てられた条件、次のコンテキスト、トラップの判定)への全域関数であり、隠れた状態を持ちません。
- 結果は仕様のものとビット単位で一致し、固定された公式テストスイートに対して検査されています(適合性を参照)。
- パッケージは IEEE 754 のパッケージ
decimalから独立しているので、一方の契約の変更がもう一方に漏れ出すことはありません。
数学的背景
数、コホート、調整指数
有限の GDA 数は、符号 、係数 、指数 からなる三つ組 であり、次の値を表します。
写像 は単射ではありません: と はどちらも を表します。同じ値を持つ三つ組はコホートをなします。GDA は値だけでなく三つ組を保持します。指数が意味(量子 :「2.50」はセント単位まで計測された)を持つからです。ゼロは符号を持つので、 は です。 に対し、 の 10 進桁数を と書き、
を調整指数、すなわち先頭の桁の指数とします:。有限の数のほかに、 と、符号とペイロード を持つ quiet NaN および signaling NaN があります。
コンテキストと表現可能な集合
コンテキストは、精度 、丸めモード、指数範囲 、クランプビットを定めます。有限の数がそのコンテキストで表現可能であるとは、
かつ、クランプが有効なら でもあることです。値 は最小単位の指数です。 で 桁の数は を持ち、 未満の数(非正規化数の範囲)はその単位を保ったまま先頭の桁を失っていきます。 は で 桁の最大の数の指数です。クランプによって指数の集合はちょうど IEEE の交換形式が符号化できるものになります。decimal32/64/128 のプリセットがクランプするのはこのためです。
算術の規則:厳密な結果を求め、1 回だけ丸める
GDA のすべての算術演算は、同じ 2 段階の規則で定義されています。
- 厳密な数学的結果 を計算し、 を表す三つ組のうち、指数が演算の理想指数 に最も近いものを選ぶ。
- その三つ組が表現可能でなければ、それをコンテキストに丸め、何が起きたかを表す条件を立てる。
ステップ 1 は厳密なので、ステップ 2 は 1 回の丸めであり、以下のすべての限界は 1 回の丸めについての限界です。パッケージはこの規則を文字どおりに守っています。各演算はオペランドから厳密な係数と指数を構築し、その後、共通の最終化ルーチン(normalize_decimal_parts_ctx_repr に続いて finalize_finite_ctx)を 1 回呼び出します。これが、有限の結果を丸めたり、精度や範囲に関する条件を立てたりする唯一のコードです。
理想指数
理想指数とは、オペランドにある情報だけを使って厳密な結果を表す、最も粗い量子です。導出は簡単です。
加算では、 は両方のオペランドが整数となる最大の指数なので、和が量子の整数倍であることが保証される最大の指数です。このとき括弧内は厳密な整数係数になります。乗算では、係数 は指数 で整数であり、一般にそれより大きな指数では整数になりません。除算では商 が整数とは限らないので、理想指数 は到達可能な場合にだけ使われます。厳密な商は、高々 桁の整数係数を保つ範囲で に最も近い指数で書かれ、厳密でない商は 桁すべてを使います。平方根では、 は、その 2 倍が 以下で最大の偶数となる指数です。表はパッケージが使う規則をまとめたものです。
| 演算 | 理想指数 |
|---|---|
add、subtract、fma(和の部分) | |
multiply、fma(積の部分) | |
divide | 厳密なら 、そうでなければ 桁 |
sqrt | 厳密なら 、そうでなければ 桁 |
remainder, remainder_near | |
divide_integer, to_integral_* | 整数値には 、商には 0 |
quantize, rescale | 要求された指数 |
scaleb | |
整数 での power、厳密な場合 | |
exp、ln、log10、整数でない power | 桁(exp(0)、ln(1)、log10(10^k) を除き、常に厳密でない) |
plus, minus, abs, apply |
ゼロの結果には先頭の桁がないので、その指数は単に理想指数を (または )にクランプしたものです。厳密にゼロとなる和の符号は、両方のオペランドが負である場合、またはモードが Floor でどちらかが負である場合を除いて です。これは、 方向への丸めの場合を除いて とする IEEE の規則の 10 進版です。
///|
test "ideal exponents" {
let ctx = @decimal_gda.GdaContext::decimal64()
let d = (s : String) => @decimal_gda.Decimal::from_string(s).unwrap()
inspect(@decimal_gda.add(d("1.30"), d("1.2"), ctx).value(), content="2.50")
inspect(@decimal_gda.multiply(d("1.30"), d("1.2"), ctx).value(), content="1.560")
inspect(@decimal_gda.divide(d("2.40"), d("2"), ctx).value(), content="1.20")
inspect(@decimal_gda.divide(d("1"), d("4"), ctx).value(), content="0.25")
inspect(@decimal_gda.sqrt(d("1.00"), ctx).value(), content="1.0")
inspect(@decimal_gda.subtract(d("1.0"), d("1.00"), ctx).value(), content="0.00")
}
精度への丸め
厳密な結果を 、、 とします。下位 桁を割り落として 、 とします。すべての丸めモードは、インクリメント を用いて を返し、8 つのモードは だけが異なります(should_increment_decimal_repr)。
ZeroFiveUp は「保持される最後の桁が 0 または 5 でない限り、ゼロ方向へ丸める」と読みます。その目的は、後でどのモードでより少ない桁へ丸めても、最初の丸めの影響を受けないようにすることです。 と の比較は 10 進のリム上で厳密に行われます(gda_coeff_div_pow10_round_info_repr は商、 かどうか、 の符号を返します)。 であれば、結果は に再正規化されます。丸めの前に、最終化ルーチンはまず の末尾のゼロを落とそうとします。厳密な係数が長すぎる原因がゼロだけであれば、結果は厳密であり、Rounded だけが立てられます。
を結果 の最終桁の単位とします。half 系のモードでは 、それ以外では なので、
そして から が得られるので、
これは標準モデル 、 における 10 進の単位丸め誤差 です。22 N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.2. 10 進の wobble は 2 進より大きく、相対間隔は 1 つの decade の中で 10 倍変化します(2 進では 2 倍)(Goldberg 1991, §1.2)。 丸めは、 桁が取り除かれたときには常に Rounded を、取り除かれた桁にゼロでないものがあったとき()には常に Inexact を立てます。
この限界と、このページにある他の丸めに関する事実の完全な証明は、添付資料にあります。
非正規化数の結果と
厳密な結果が を持つとき、それは極小です。極小な結果が必要とする単位は高々 なので、丸めの位置は「 桁を残す」ではなく「 以上の桁を残す」になります。厳密な指数が のとき、最終化ルーチンは 桁を取り除きます(すべての桁を取り除くこともあります)。このとき絶対誤差は非正規化数の単位で抑えられます。
しかし相対誤差はもはや では抑えられません。 が から へ小さくなるにつれて、精度は 桁から 1 桁へと徐々に下がります。条件がこれを記録します。Subnormal はすべての極小な結果で立てられ(GDA、およびこのパッケージの GDA 関数は、厳密な から丸めの前に極小性を判定します)、Underflow は極小な結果が厳密でもない場合に、Clamped はゼロに丸められた場合に立てられます(そのときゼロは指数 をとります)。ステータスを持たない層では、IEEE 流の用途のために丸め後の判定(DecimalTininessDetection::AfterRounding)も提供されています。これはどの結果を極小とみなすかを変えるだけで、値は決して変えません。
クランプ
clamp が有効な場合、 だが である結果は、値としては表現可能ですが三つ組としては表現できません。 は を意味するので、係数に 個のゼロを付け足すと
となり、付け足した三つ組は高々 桁で、値も同じです。パッケージはまさにこれを行い、Clamped を立てます。値は変わらず、コホートのメンバーだけが異なります。ゼロも同様に、指数を の中へ移すことでクランプされます。
オーバーフロー
丸めた結果が を持つとき、演算はオーバーフローします。Overflow、Inexact、Rounded が立てられ、結果は か、同じ符号の最大の有限数 のどちらかになります。どちらになるかは、 を の先にある表現可能な数とみなし、モード自身の方向を適用することで決まります。half 系のモードと Up はゼロから遠ざかる方向に動くので に達し、Down と ZeroFiveUp はゼロ方向に動くので(ZeroFiveUp がゼロから遠ざかる方向に丸めるのは最後の桁が 0 または 5 の場合だけで、 の最後の桁は 9 です) で止まります。Ceiling は正の結果に対して 、負の結果に対して を与え、Floor はその鏡像になります(overflow_to_infinity)。
| モード | 正のオーバーフロー | 負のオーバーフロー |
|---|---|---|
HalfEven, HalfUp, HalfDown, Up | ||
Down, ZeroFiveUp | ||
Ceiling | ||
Floor |
条件、シグナル、トラップ
GDA は、条件(何が起きたか:ゼロ除算、結果が丸められた、変換の入力が不正だった、…)、シグナル(条件が引き起こす名前付きのイベント)、トラップ(ユーザーが計算を中断するよう求めたシグナル)を区別します。パッケージは条件ごとに 1 つのフラグを保持し、1 つの規則で条件を GDA のシグナルに対応付けます。4 つの詳細な無効条件 ConversionSyntax、DivisionImpossible、DivisionUndefined、InvalidContext はすべて InvalidOperation をシグナルします。形式的には、 を 13 個のフラグ、 を無効系統の 5 個のフラグとします。フラグ集合 とシグナル に対して、
これが GdaFlags::contains です。このとき演算は次の状態機械になります。
ここで は、オペランドとポリシー (精度、丸め、指数範囲、クランプ、extended)だけから計算される結果と立てられたフラグであり、
は、優先順位リスト InvalidOperation, DivisionByZero, DivisionUndefined, DivisionImpossible, InvalidContext, ConversionSyntax, Overflow, Underflow, Subnormal, Inexact, Rounded, Clamped, LostDigits のうち、 かつ を満たす最初のシグナル、そのようなものがなければ です(complete_gda と trapped_signal)。
定義から直ちに 3 つの性質が従い、ユーザーはこれらに依拠しています。
- 値はステータスにもトラップにも依存しない。 と は の関数であり、ステータスとトラップは と にしか関与しません。したがってトラップを有効にしても結果が変わることはなく、
Trappedは定義済みの結果を運ぶことができます。 - ステータスは単調かつ冪等である。 であり、 は結合的、可換、冪等なので、受け渡された一連の演算の後のステータスは、立てられたすべての集合の和集合であり、列のグループ化の仕方に依存しません。
- トラップの選択は決定的である。 優先順位リストはシグナル上の全順序なので、条件がどの順序で検出されたかにかかわらず、1 つの演算が選ぶトラップは高々 1 つです。
///|
test "status is the union of the raised sets" {
let ctx = @decimal_gda.context(precision=3)
let d = (s : String) => @decimal_gda.Decimal::from_string(s).unwrap()
let a = @decimal_gda.divide(d("1"), d("3"), ctx) // Inexact, Rounded
let b = @decimal_gda.divide(d("1"), d("0"), a.next_context()) // DivisionByZero
let expected = a.raised().combine(b.raised())
inspect(b.next_context().status() == expected, content="true")
// Trapping changes the variant, never the value.
let trapping = ctx.trap(Inexact)
let t = @decimal_gda.divide(d("1"), d("3"), trapping)
inspect(t.value() == a.value(), content="true")
inspect(t is @decimal_gda.GdaOutcome::Trapped(Inexact, _, _, _), content="true")
}
設計上の判断
ステータスはグローバル状態ではなく GdaOutcome で受け渡す
問題。 GDA 仕様はコンテキストを、演算がステータスフラグを設定する可変オブジェクトとして記述しています。ほとんどの実装(decNumber、Python の decimal)はスレッドごとにカレントコンテキストを保持します。
選択肢。 (a) 参照で渡される可変なコンテキスト。(b) スレッドローカルまたはグローバルなカレントコンテキスト。(c) すべての結果とともに返される不変なコンテキスト。
選択:(c)。 すべての GDA 関数は GdaOutcome を返し、呼び出し側は next_context() を次の演算に渡します。理由は上記の性質です。 はポリシーしか読まないので、状態機械は純粋な数値計算の部分と純粋な記録管理の部分に分解され、どちらも参照透過です。式は、結果やフラグを変えることなく、再評価したり、メモ化したり、別のスレッドで実行したりできます。テストランナーはコンテキストのスナップショットを取り、そのもとで同じ演算を何度も実行できます。frontend/gda_expr の .decTest ランナーはまさにそれを行っています。また MoonBit にはスレッドローカルストレージがないので、(b) はプロセス全体のグローバル状態を意味することになり、Luna-Flow はそれを排除しています。代償は明示的な受け渡しですが、線形なパイプラインについては decimal_gda_checked パッケージがそれを不要にします。何も立てられなかった場合は入力のコンテキストがそのまま返されるので、厳密な演算が新しいコンテキストを割り当てることはありません。
トラップの定義済みの結果を保持する
GDA ではトラップは制御を移しますが、仕様は演算が返したはずの結果をそれでも定義しています。トラップをエラー(Result::Err、raise)として表現すると、その値が捨てられてしまいます。Trapped は値、次のコンテキスト、立てられた集合を保持するので、呼び出し側はそれを検査したり、ログに記録したり、そこから再開したりできます。性質 1 により、それはトラップがない場合と同じ値であることが保証されます。
詳細な無効条件を保持する
仕様は ConversionSyntax、DivisionImpossible、DivisionUndefined、InvalidContext を InvalidOperation を通じて報告します。パッケージはこれらを別々のフラグとして保持し、かつそれぞれについて contains(InvalidOperation) を真にし、いずれかが立てられたときは常にステータスに invalid_operation を設定します。8 つの GDA シグナルしか知らないプログラムにはちょうど GDA の振る舞いが見え、一方でテストハーネスや診断では不正なリテラルと を区別できます。InvalidOperation を優先順位リストの先頭に置くことで、仕様が要求するとおり、InvalidOperation のトラップが 4 つすべてを捕捉します。
1 つの最終化ルーチン、特殊値を先に
各演算はまず特殊なケース(NaN の伝播、無効演算、無限大、厳密なゼロ)を決定し、次に厳密な有限の係数と指数を構築し、その後で初めて最終化ルーチンを呼び出します。係数のアルゴリズムがフラグを設定することはありません。これにより、条件は厳密な結果とポリシーの関数となり、どの乗算・除算カーネルが選ばれたかに依存しません。また、標準に関わる振る舞いに触れずにカーネルを調整できます。
decimal から独立した GDA パッケージ
IEEE 754-2008 の 10 進算術は GDA から発展したものなので、両者はほとんどの有限の結果で一致しますが、契約は異なります。
| 観点 | decimal_gda (GDA 1.70) | decimal (IEEE 754-2019) |
|---|---|---|
| 精度 | コンテキストごとに任意の | 形式の (または選択した値) |
| 丸めモード | HalfUp、HalfDown、ZeroFiveUp を含む 8 種類 | IEEE の属性 |
| 条件 | コンテキスト内のスティッキーなステータス、定義済みの結果を伴うトラップ | 値とともに返される演算ごとのフラグ |
詳細な無効条件、LostDigits、サブセット算術 | あり | いいえ |
| 極小性 | 丸め前 | 選択可能 |
| 初等関数 | sqrt, exp, ln, log10, power | IEEE の推奨演算の集合 |
| 交換形式 | DPD | DPD と BID |
共通のコアを持つと、両方の状態モデルの和を抱え込まなければならず、一方の標準のために行った変更が、もう一方の結果を黙って変えてしまうおそれがあります。そのためパッケージは独自の値の型、係数カーネル、コンテキスト、最終化ルーチン、DPD コーデックを持ち、本番用の依存関係スキャンによって decimal を決してインポートしないことを検査しています。係数の閾値は現在 decimal のものと等しいですが、これは計測による偶然の一致であり、共有された依存関係ではありません。
ステータスを持たない層(DecimalContext、DecimalFlags、*_ctx メソッド)は、パッケージ自身のエンジンを公開したものです。IEEE のように演算ごとにフラグを返すのでアダプタにとって便利です(Luna-Flow/arithmetic のトレイト実装はこれを使っています)が、実装しているのは GDA の算術です。IEEE 754 の契約は decimal にあります。
等価性が証明された高速経路
2 種類の近道が、観測可能な結果を何も変えずに汎用の機構を回避します。
小さな厳密な整数。 parse、add、subtract、multiply、fma はまず次の条件を確認します。すべてのオペランドが指数 0 で係数が 未満の整数であること、厳密な結果が 未満で高々 桁のゼロでない整数であること、その調整指数が にあること、クランプによって指数 0 が許されること、そしてコンテキストが extended であることです。これらの述語のもとでは、汎用の経路は丸めを行わず、条件を立てず、指数 0(これらの演算すべての理想指数)を返すので、近道は同じ で Completed(v, C, none) を返します。ゼロの結果は、その符号が丸めモードに依存するため除外されます。
吸収される加数。 と、 を満たすはるかに小さい を加えるとき、厳密な和を作る必要はありません。 のすべての桁は の保持される最後の桁より下にあるので、 が丸めに影響するのは、その符号と半単位との比較を通じてだけです。
これは、上の丸めの表で を同じ比較クラスのスティッキーな代表値に置き換えたものです。exact_base_small_addend_result はまさにそれを評価し、compare_magnitude_to_half_ulp が と を厳密に比較します。 が 10 のべきで が逆の符号を持つ特別な場合( のすぐ下の単位が 10 分の 1 になる)は除外され、汎用の経路をとります。
除算
divide は、、、10 のべきによる厳密な除算、小さな厳密な除数による除算を別々に扱います。それ以外の場合は、被除数を として でスケールし、 が少なくとも 桁の整数部を持つようにします。そして をコンテキストのモードで整数 に丸め、さらに を 桁に丸めます。商が有限小数になるかどうかは厳密に判定され(has_finite_decimal_expansion_repr: が有限小数になるのは が 2 と 5 以外の素因数を持たないとき、かつそのときに限る)、それによって厳密な商には理想指数のコホートが選ばれ、それ以外には 桁の係数で Inexact が強制されます。
方向付き丸めのモードでは、切り捨ては合成できるので、2 回の連続した丸めは 1 回の丸めと等価です:。half 系のモードでは、1 回目の丸めがタイを作り出す場合、すなわち でありながら の捨てられる桁がちょうど になる場合を除いて等価です。
平方根
sqrt はまず厳密な平方根を試みます。末尾のゼロを取り除き、指数を偶数にし、Newton 反復 によって となる整数平方根 を求め、 なら受理し、その後 桁の範囲で理想指数 に向けてゼロを付け足します。そうでない場合は、最終的な位置で直接丸めます。平方根の指数を として を計算し、被開平数と中点の 2 乗を比較してインクリメントを決めます。これは整数で厳密に行えます。
(まず 桁で、次に で丸めるのではなく) で 1 回だけ丸めることで、極小な平方根の二重丸めを避けています。厳密な平方根は先に処理されているので、この比較が判定するのは無理数の平方根だけであり、それが中点に等しくなることはありえません。GDA 関数は常に最近接偶数丸めを行います。
整数べき
整数の指数 に対しては、結果は GDA 仕様の規定どおりに計算されます。厳密なべきが 桁に収まれば、指数 で厳密に返されます。そうでなければ、作業精度 (サブセットコンテキストでは 1 桁少ない)で 2 進べき乗法を実行し、各積の後で最近接偶数丸めを行います。負の は から始め、最後の積をコンテキストへ丸めます。 個の因子はそれぞれ高々 回の丸めを経るので、作業結果は となり、次が成り立ちます。22 N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.2. 10 進の wobble は 2 進より大きく、相対間隔は 1 つの decade の中で 10 倍変化します(2 進では 2 倍)(Goldberg 1991, §1.2)。
これは結果の最終桁の単位で高々 です。最後の丸めを加えると、厳密でない整数べきは、half 系のモードで最終桁の単位の 以内になります。これは正しい丸めに近いものの正しい丸めではありません。仕様も整数べきについてそれ以上は要求していません。結果が必ずオーバーフローまたはアンダーフローするほど大きな指数は、結果の調整指数に対する限界 と から事前に検出されます。
正しく丸められた exp、ln、log10 と整数でない power
これらの関数は、認証付きの区間演算と Ziv 流の精緻化ループによって評価されます。33 A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. ボール演算については J. van der Hoeven, “Ball arithmetic”, 2009、および F. Johansson, “Arb: efficient arbitrary-precision midpoint-radius interval arithmetic”, IEEE Trans. Computers 66(8), 2017 を参照してください。 について:
- まず厳密なケース。 、、、特殊なオペランド、定義域エラー、そして
powerが厳密に決定できるケース(sqrtによる 、10 のべき、、確実なオーバーフローまたはアンダーフロー)はループに到達しません。 - 入力を包含する。 は方向付き丸め( 方向と 方向への
to_bin_float)によって ビットの 2 進ボール に変換されるので、厳密に となります。 - 出力を包含する。
ball_floatはボール上で を評価し、 を返します(log10では を のキャッシュされた包含区間で割り、powerでは の包含区間を通じて を計算します)。 - 候補を認証する。 10 進の近似値 (ボールの中心、または 10 進級数による評価)を に丸めます。 を、 に丸められる実数からなる のまわりの開区間とします。half 系のモードでは隣接値との 2 つの中点、方向付き丸めのモードでは または です。その端点は厳密な 10 進数であり、それぞれを 2 進で包含し、次が成り立つとき結果を受理します。 これにより が証明されます。2 つ目の判定では、厳密な 2 進有理数の端点 と の両方をコンテキストへ丸めます。すべての丸めモードは単調なので、 からも が証明されます。
- 精緻化する。 そうでなければ作業精度を と増やします。これを最大 12 回行います。
開始精度は として ビットです。10 進 1 桁あたり 4 ビットは を上回るので、入力は 10 進の情報を失わずに運ばれ、64 ビットのガードビットが残ります。 の広いクランプなしのコンテキストで の引数に対しては、約 ビットを使う安価な初回試行を行い、認証できなければ にフォールバックします。
自身が表現可能な数でも中点でもなければ、包含区間が 1 点に縮むのでループは停止します。exp、ln、log10 では、ステップ 1 の後これは常に成り立ちます。Lindemann–Weierstrass の定理により、有理数 に対して は超越数であり、したがって有理数 に対して は無理数です。また が有理数になるのは 10 の整数べきの場合だけです。表現可能な数と中点は有理数です。それでも予算を使い切った場合(たとえば厳密な値が中点で、ステップ 1 で捕捉されない power)、try_* メソッドは認証の失敗を返し、GDA 関数は InvalidOperation を伴う NaN を返します。証明されていない結果を返すことは決してありません。
GDA 関数 exp、ln、log10、sqrt は、丸めを HalfEven にしたコンテキストのコピーを渡します。仕様がこれらの関数を、コンテキストのモードにかかわらず最近接偶数丸めで正しく丸められるものとして定義しているからです。power は、仕様とそのテストベクタが要求するとおりコンテキストのモードを使い、整数でないべきについては、たまたま厳密であっても 桁で Inexact として報告します(decimal_power_noninteger_gda_result)。さらに 2 つの GDA の規則を文字どおりに実装しています。これらの関数は 、、 が高々 999,999 のコンテキストでのみ定義され(math_context_is_restricted、それ以外では InvalidContext を立てます)、サブセット算術では ln は古典的な参照アルゴリズムの結果を再現します。これは正しく丸められた結果より最終桁の単位で 1 大きくなることがあります(decimal_ln_subset_result)。従来のサブセットのテストベクタがその結果を記録しているためです。
係数の表現とカーネル
係数は、 未満の値に対する Small(UInt64) か、桁数をキャッシュした基数 のリムの配列のどちらかで、どちらも永続的です。演算がオペランドのリムを書き換えることはありません。10 進のリムにより、桁数の計算、末尾のゼロの除去、半単位との比較、ZeroFiveUp の最終桁の判定が、2 進から 10 進への変換なしに定数時間または線形時間で行えます。乗算と除算は、リム数に応じて、筆算、Karatsuba、Toom-3、2 法 NTT、Knuth の Algorithm D、Burnikel–Ziegler、Newton の逆数除算から選択します。閾値はターゲットごとに次のとおりです。
| ターゲット | 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 |
閾値は計測に基づく性能上の方針であり、意味論ではありません。どのカーネルも厳密な積または商を返すので、選択によって結果やフラグが変わることはありません。計測結果は性能を参照してください。
正しさ/不変条件
- 1 回だけの丸め。 整数の
powerと上記の除算のケースを除くすべての有限の結果は、厳密な結果を 1 回だけ丸めたものです。したがって half 系のモードでは 、それ以外では であり、非正規化数の範囲外では です。 - 理想のコホート。 厳密な結果は、その理想指数に最も近い表現可能な指数で返されるので、厳密なデータに対する演算は量子を保ちます(
2.50 × 3 = 7.50)。 - ステータスからの独立性。 はポリシーを通じてのみ に依存します。結果がスティッキーなステータスやトラップに依存することはありません。
- ステータスの代数。 ステータスは増えるだけであり、受け渡された一連の演算の後のステータスは、立てられた集合の和集合です。
- トラップの決定性。 1 つの演算で発火するトラップは高々 1 つで、固定された全順序によって選ばれます。
TrappedはCompletedが運ぶはずの値と同じ値を運びます。 - 認証付きの初等関数。
exp、ln、log10、整数でないpowerの有限で厳密でない結果は、それが正しく丸められた値であることの証明(添付資料の補題 3 または 4)とともにしか返されません。証明できなければ NaN とInvalidOperationになります。 - 全順序。
compare_totalは三つ組上の全順序(符号、次にクラス、次に値、次に指数、次にペイロード)であり、Decimal::compareは NaN がすべての数より上の 1 つのクラスをなす全前順序です。 - 証拠。 固定された
officialテストスイートでは、正当な実行可能スカラー行 64,986/64,986 が、従来のofficial0スイートでは 16,124/16,124 が合格しています(適合性)。これらは有限の主張です。上記の除算の欠陥と、API ページに記載されている to-integral の相違は、固定されたどの行でもカバーされていません。
却下した代替案
- 可変またはグローバルなカレントコンテキスト。 結果とフラグが評価順序と隠れた状態に依存してしまうため不採用としました(最初の設計判断を参照)。
- エラーとしてのトラップ。 GDA で定義された結果が失われるため不採用としました。
decimalとエンジンを共有すること。 一方の標準の変更がもう一方の結果を変えうるため不採用としました。計測された重複は、偶発的な意味論上の結合よりも安上がりです。- 固定数のガード桁を持つ 2 進浮動小数点による初等関数。 固定数のガード桁では正しい丸めを保証できないため不採用としました(table-maker’s dilemma)。精緻化を伴う認証は正しい丸めを保証し、できない場合は目に見える形で失敗します。
- すべての結果を正規化すること。 量子は GDA の数の一部なので不採用としました。正規化は
reduceとして明示的に利用できます。
境界
- パッケージは GDA の演算だけを実装し、それ以外は何も実装しません。仕様外の三角関数、双曲線関数、その他の関数はなく、ステータスを持たない層のわずかな追加機能を除いて IEEE 754 の演算もありません。
.decTestファイルの解析、テストスイートの実行、ファイルの読み込みは行いません。それらはfrontend/gda_exprとリポジトリのツールの役割です。- BID の交換形式は提供しません。DPD のみです。
- 整数べきについて GDA の要求を超える正しい丸めは提供しません。また現在のブランチでは、上記の half 系モードでの除算のケースについても正しい丸めを提供しません。
- 可変またはグローバルなコンテキストは提供せず、コンテキストは同一性を持ちません。同じフィールドを持つ 2 つのコンテキストは交換可能です。
Footnotes
-
M. F. Cowlishaw, General Decimal Arithmetic Specification, version 1.70 (2009), https://speleotrove.com/decimal/decarith.html。テストスイートは同じ著者による
dectestコレクションのバージョン 2.62 です。 ↩ -
N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.2. 10 進の wobble は 2 進より大きく、相対間隔は 1 つの decade の中で 10 倍変化します(2 進では 2 倍)(Goldberg 1991, §1.2)。 ↩ ↩2
-
A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. ボール演算については J. van der Hoeven, “Ball arithmetic”, 2009、および F. Johansson, “Arb: efficient arbitrary-precision midpoint-radius interval arithmetic”, IEEE Trans. Computers 66(8), 2017 を参照してください。 ↩