dzmingli_vs_floating チュートリアル

このチュートリアルでは、DzmingLi/decimal と floating の decimal_gda を厳密なオラクルで検査する方法を、まず演算一つ、次に決定的なコーパスについて示し、テストから小さな Mare Mark 計測を実行する方法、公開されたベンチマークとその公式 decTest 監査を再現する方法を説明します。検査の背後にある数学は設計ページにあります。

クイックスタート

diff_bench は GitHub だけのリポジトリで、mooncakes には公開されていません。クローンしてモジュール内で作業するか、クローンを moon.work ワークスペースに加えてください:

git clone https://github.com/Luna-Flow/diff_bench.git
cd diff_bench
moon test --target native

モジュール内のパッケージは moon.pkg でベンチマークパッケージをインポートします:

import {
  "Luna-Flow/diff_bench/dzmingli_vs_floating",
}

最小限の役に立つプログラムは、両ライブラリでの除算一つをオラクルで検査します:

test "quick start" {
  let a = @dzmingli_vs_floating.parse_decimal_value("1.25")
  let b = @dzmingli_vs_floating.parse_decimal_value("8")
  let fixture = @dzmingli_vs_floating.prepare_fixture(Divide, a, b)
  let expected = @dzmingli_vs_floating.oracle_operation(Divide, a, b).canonical
  let show = (o : @dzmingli_vs_floating.DecimalObservation) => {
    @dzmingli_vs_floating.canonical_string(@dzmingli_vs_floating.canonical_observation(o))
  }
  inspect(expected, content="0.15625")
  inspect(show(@dzmingli_vs_floating.run_dz(fixture)), content="0.15625")
  inspect(show(@dzmingli_vs_floating.run_gda(fixture)), content="0.15625")
}

三つの inspect の行が出力です。オラクルと両ライブラリは 1.25/8=0.156251.25 / 8 = 0.15625 で一致します。

日常的な作業

一つの入力ですべての演算ファミリーを検査する

oracle_operation3 と prepare_fixture3 は Fma のために第三オペランドを取り、他の演算はそれを無視します。以下のループは、すべての演算に適した入力(SquareRoot には完全平方数、Power には整数の指数、ScaleB には 0 のシフト)で 16 演算を検査します。

test "every operation on one input" {
  let ops : Array[@dzmingli_vs_floating.Operation] = [
    Add, Subtract, Multiply, Divide, DivideInteger, Remainder, Power, Fma,
    SquareRoot, Plus, Minus, Abs, Reduce, ToIntegralExact, ToIntegralValue,
    Compare,
  ]
  let left = @dzmingli_vs_floating.parse_decimal_value("144")
  let two = @dzmingli_vs_floating.parse_decimal_value("2")
  let third = @dzmingli_vs_floating.parse_decimal_value("0.5")
  let mut agreed = 0
  for op in ops {
    let expected = @dzmingli_vs_floating.oracle_operation3(op, left, two, third).canonical
    let fixture = @dzmingli_vs_floating.prepare_fixture3(op, left, two, third)
    for observation in [
      @dzmingli_vs_floating.run_dz(fixture),
      @dzmingli_vs_floating.run_gda(fixture),
    ] {
      let got = @dzmingli_vs_floating.canonical_observation(observation)
      if @dzmingli_vs_floating.canonical_string(got) == expected {
        agreed += 1
      }
    }
  }
  inspect(agreed, content="32")
}

16 演算すべてが両ライブラリで一致します(16×2=3216 \times 2 = 32)。

解析を含めて、または含めずに計時する

run_dz と run_gda は計時前に解析したオペランドを使い、run_dz_full と run_gda_full は先に正準文字列を解析します。両方のパスは同じ値を返さなければならず、FullPath の計時範囲はそれを前提にしています:

test "both timing paths agree" {
  let a = @dzmingli_vs_floating.parse_decimal_value("123456789.000000018")
  let b = @dzmingli_vs_floating.parse_decimal_value("-0.987654321")
  let fixture = @dzmingli_vs_floating.prepare_fixture(Multiply, a, b)
  let show = (o : @dzmingli_vs_floating.DecimalObservation) => {
    @dzmingli_vs_floating.canonical_string(@dzmingli_vs_floating.canonical_observation(o))
  }
  let fast = show(@dzmingli_vs_floating.run_gda(fixture))
  inspect(fast, content="-121932631.112635286777777778")
  inspect(show(@dzmingli_vs_floating.run_gda_full(fixture)) == fast, content="true")
  inspect(show(@dzmingli_vs_floating.run_dz_full(fixture)) == fast, content="true")
  inspect(@dzmingli_vs_floating.oracle_operation(Multiply, a, b).canonical == fast, content="true")
}

決定的なコーパスを検証する

generate_cases は同じシードに対して同じオペランドを返します。こうしたコーパスの除算は有限小数にならないことがあるので、ループは結果が常に厳密な演算だけを検査します:

test "deterministic corpus" {
  let ops : Array[@dzmingli_vs_floating.Operation] = [Add, Subtract, Multiply, Compare]
  let mut checked = 0
  for op in ops {
    for case in @dzmingli_vs_floating.generate_cases(73, 20, op) {
      let left = @dzmingli_vs_floating.parse_decimal_value(case.left)
      let right = @dzmingli_vs_floating.parse_decimal_value(case.right)
      let expected = @dzmingli_vs_floating.oracle_operation(op, left, right).canonical
      let fixture = @dzmingli_vs_floating.prepare_fixture(op, left, right)
      let gda = @dzmingli_vs_floating.canonical_observation(@dzmingli_vs_floating.run_gda(fixture))
      let dz = @dzmingli_vs_floating.canonical_observation(@dzmingli_vs_floating.run_dz(fixture))
      assert_eq(@dzmingli_vs_floating.canonical_string(gda), expected)
      assert_eq(@dzmingli_vs_floating.canonical_string(dz), expected)
      checked += 1
    }
  }
  inspect(checked, content="80")
}

精度不足がどう見えるか

フィクスチャの精度は、どの結果も丸められないように選ばれています。小さすぎれば 0 方向の丸めで結果が短くなり、オラクルとの比較は失敗します。ここでは floating の decimal_gda パッケージ(@decimal_gda としてインポート)を使い、有効 6 桁の積を 4 桁のコンテキストで計算します:

test "too little precision is detected" {
  let a = @dzmingli_vs_floating.parse_decimal_value("123.45")
  let b = @dzmingli_vs_floating.parse_decimal_value("6.7")
  let context = @decimal_gda.context(precision=4, rounding=@decimal_gda.GdaRoundingMode::Down)
  let product = @decimal_gda.multiply(
    @dzmingli_vs_floating.gda_from_neutral(a, 8),
    @dzmingli_vs_floating.gda_from_neutral(b, 8),
    context,
  )
  let got = @dzmingli_vs_floating.canonical_observation(Gda(product))
  inspect(@dzmingli_vs_floating.canonical_string(got), content="827.1")
  inspect(@dzmingli_vs_floating.oracle_operation(Multiply, a, b).canonical, content="827.115")
  inspect(@dzmingli_vs_floating.working_precision(Multiply, a, b), content="9")
}

working_precision は 9 桁を要求し、827.115827.115 の 6 桁には十分です。

小さな Mare Mark 計測を実行する

run_mare_benchmark は各データセットをオラクルで検証してから両ライブラリを計時します。async なので、native か js ターゲットで async test から呼び出してください。smoke_protocol は実行を短く保ちます:

async test "smoke measurement" {
  let report = @dzmingli_vs_floating.run_mare_benchmark(
    [Add, Multiply],
    FullPath,
    @dzmingli_vs_floating.expand_digit_scales([16], 2),
    @dzmingli_vs_floating.smoke_protocol(),
    7UL,
  )
  inspect(report.failed_count, content="0")
  inspect(report.validation_count, content="8")
  inspect(report.results.length(), content="2")
  inspect(report.results[0].samples, content="6")
}

2 演算 × 2 データセット × 2 実装で 8 件の検証になります。各結果行は 2 データセット × スモークの繰り返し 3 回 = 6 サンプルを対にし、レイテンシは report.results[i].dz_median_us と gda_median_us です。

公開されたベンチマークを再現する

リポジトリのルートで実行します。

moon run --release src/dzmingli_vs_floating/bench --target native \
  | sed -n '/^{/p' > artifacts/dzmingli_vs_floating/scaling.jsonl
moon run --release src/dzmingli_vs_floating/bench_common --target native \
  | sed -n '/^{/p' > artifacts/dzmingli_vs_floating/common_digits.jsonl

スケーリングの実行は長時間かかり、DzmingLi が 4,096 桁から検証に失敗するため、完全なレポートを書いた後で 0 以外の状態で終了します。数値を読む前にすべての Mare Mark の summary レコードが "complete":true であることを確認し、ホストを記録するために MARE_CPU、MARE_OS、MARE_BUILD_MODE を設定してください。出力は bench のページで説明しています。

公式の GDA decTest 監査は別に実行します:

sh tools/run_dzmingli_dectest_audit.sh

decTest のアーカイブをダウンロードして SHA-256 を確認し、共通の演算ファイルを両ライブラリに対して実行します。DzmingLi の既知の toSci の失敗が残っている間は 0 以外の状態で終了します。

さらに進んで

独自のオペランドクラス。 prepare_fixture3 はオペランドから精度を選びます。精度契約が証明されているのは生成されるクラスだけなので、新しい入力では厳密な結果が収まるか確認してください。収まらなければ検証は黙って通らずに失敗するので、新しいフィクスチャの失敗はライブラリより先にフィクスチャを疑うべきかもしれません。

演算の追加。 Operation にコンストラクタを、operation_name に名前を、oracle_operation3 に規則を、working_precision に精度の行(または prepare_fixture3 での上書き)を、両アダプターに呼び出しを、Mare Mark の具体化処理に入力の規則を加えます。計時する前に、境界値で新しい演算をオラクルと照合するテストを追加してください。

図。 tools/ の Python レイアウトは JSONL を読み、Mare Mark の Plot IR を作り、Matplotlib で PNG、PDF、SVG を描きます:

python3 tools/plot_dzmingli_benchmark.py \
  artifacts/dzmingli_vs_floating/scaling.jsonl \
  --output artifacts/dzmingli_vs_floating/main \
  --ir-output artifacts/dzmingli_vs_floating/main.ir.json

tools/plot_dzmingli_supplementary_benchmark.py も同じ方法で補足の図を描きます。

兄弟パッケージ。 floating_vs_decmial_x は同じ手法を moonbitlang/x/decimal に適用します。ライブラリは floating から、ランナーは mare_mark から来ています。

よくある落とし穴

  • oracle_divide は 1/31/3 のような循環小数の商で中断します。約分後の分母が 22 と 55 の積になる除数だけを使ってください。
  • oracle_operation は加数 0 を渡すので Fma では誤りです。oracle_operation3 を使ってください。
  • 比較は指数とフラグを無視します。GDA の 1.20 と DzmingLi の 1.2 は一致します。表現と条件には decTest 監査を使ってください。
  • Parse と Format はどちらの側でも準備済みのオペランドを返すだけで、解析や書式化は計測しません。
  • validation_count は失敗した検証を含みます。合格した検証は validation_count - failed_count です。
  • run_mare_benchmark は検証失敗で中断しないので、failed_count を確認してください。実行ファイルはレポートを書いた後で中断します。
  • 実行ファイルが実際に仕事をするのは --target native のときだけで、他のターゲットではメッセージを表示して終了します。
  • wasm-gc で長いテスト入力を BigInt::from_string で作らないでください。現在の moonbitlang/core では数千桁の文字列に対して誤った値を返します(9 を 4,096 個並べると 4,094 桁の数になります)。parse_decimal_value は自分で桁を累積するので影響を受けません。パッケージのテスト division precision covers exact terminating quotients は from_string を使いますが下界しか検査しないので、どのターゲットでも通ります。
  • DzmingLi は積が約 21,475 桁を超えると中断します(設計ページを参照)。乗算の入力は 10,000 桁以下にしてください。

次のステップ