floating_vs_decmial_x チュートリアル

このチュートリアルでは、二つの意味論グループそれぞれで moonbitlang/x/decimal(X)と floating の decimal_gda(GDA)を比較し、オラクルで検査し、小さな Mare Mark 計測を実行し、公開されたベンチマークを再現する方法を示します。グループと精度をこう選ぶ理由は設計ページにあります。

クイックスタート

このリポジトリは 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/floating_vs_decmial_x",
}

最小限の役に立つプログラムは、両ライブラリで X のポリシーに従って 11 を 33 で割ります:

test "quick start" {
  let one = @floating_vs_decmial_x.parse_decimal_value("1")
  let three = @floating_vs_decmial_x.parse_decimal_value("3")
  let fixture = @floating_vs_decmial_x.prepare_fixture(Divide, one, three, semantics=XCompatible)
  let show = (o : @floating_vs_decmial_x.DecimalObservation) => {
    @floating_vs_decmial_x.canonical_string(@floating_vs_decmial_x.canonical_observation(o))
  }
  inspect(show(@floating_vs_decmial_x.run_x(fixture)), content="0.3333333333333333333333333333")
  inspect(show(@floating_vs_decmial_x.run_gda(fixture)), content="0.3333333333333333333333333333")
}

どちらも小数 28 桁に切り捨てた商を返します。GDA は divide の後に quantize を行ってそこに至ります。

日常的な作業

意味論グループを選ぶ

ExactOverlap は両ライブラリが厳密な結果を表現できる入力のためのものです。このとき GDA は演算を一回だけ行い、厳密な値を返さなければなりません:

test "exact overlap" {
  let a = @floating_vs_decmial_x.parse_decimal_value("12345.6789")
  let b = @floating_vs_decmial_x.parse_decimal_value("8")
  let fixture = @floating_vs_decmial_x.prepare_fixture(Divide, a, b, semantics=ExactOverlap)
  let gda = @floating_vs_decmial_x.canonical_observation(@floating_vs_decmial_x.run_gda(fixture))
  inspect(@floating_vs_decmial_x.canonical_string(gda), content="1543.2098625")
  inspect(@floating_vs_decmial_x.working_precision(Divide, a, b, semantics=ExactOverlap), content="12")
}

XCompatible は X の 28 桁の切り捨てを再現します。小数 36 桁の積は両側で 28 桁に切り詰められます:

test "x-compatible product" {
  let a : @floating_vs_decmial_x.DecimalValue = { coefficient: 123456789N, scale: 18 }
  let b : @floating_vs_decmial_x.DecimalValue = { coefficient: 987654321N, scale: 18 }
  let fixture = @floating_vs_decmial_x.prepare_fixture(Multiply, a, b, semantics=XCompatible)
  let show = (o : @floating_vs_decmial_x.DecimalObservation) => {
    @floating_vs_decmial_x.canonical_string(@floating_vs_decmial_x.canonical_observation(o))
  }
  inspect(show(@floating_vs_decmial_x.run_x(fixture)), content="0.0000000000000000001219326311")
  inspect(show(@floating_vs_decmial_x.run_gda(fixture)), content="0.0000000000000000001219326311")
}

厳密な積は 1.21932631112635269×10−191.21932631112635269 \times 10^{-19} で、切り捨てによって 10−2810^{-28} の位までの桁が残ります。

コーパスをオラクルで検査する

oracle_operation は X のポリシーを実装しているので、両グループの参照になります:

test "corpus against the oracle" {
  let ops : Array[@floating_vs_decmial_x.Operation] = [Add, Subtract, Multiply, Divide, Compare]
  let mut checked = 0
  for op in ops {
    for case in @floating_vs_decmial_x.generate_cases(73, 20, op) {
      let left = @floating_vs_decmial_x.parse_decimal_value(case.left)
      let right = @floating_vs_decmial_x.parse_decimal_value(case.right)
      let expected = @floating_vs_decmial_x.oracle_operation(op, left, right).canonical
      let fixture = @floating_vs_decmial_x.prepare_fixture(op, left, right)
      for observation in [
        @floating_vs_decmial_x.run_x(fixture),
        @floating_vs_decmial_x.run_gda(fixture),
      ] {
        let got = @floating_vs_decmial_x.canonical_observation(observation)
        assert_eq(@floating_vs_decmial_x.canonical_string(got), expected)
      }
      checked += 1
    }
  }
  inspect(checked, content="100")
}

生成されるケースは高々 24 桁で、どの除算の精度よりも短いので、オペランドの丸めの制限は当てはまりません。

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

async test "smoke measurement" {
  let report = @floating_vs_decmial_x.run_mare_benchmark(
    [Add, Multiply],
    ExactOverlap,
    [4, 16],
    @floating_vs_decmial_x.smoke_protocol(),
    42UL,
  )
  inspect(report.failed_count, content="0")
  inspect(report.validation_count, content="8")
  inspect(report.results[0].timing_scope, content="arithmetic_only")
}

native か js で実行してください。この関数は async です。各結果行には X と GDA のマイクロ秒単位の中央値と、GDA の中央値を X の中央値で割った x_speedup_vs_gda があります。

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

リポジトリのルートで:

moon run --release src/floating_vs_decmial_x/bench --target native \
  > artifacts/floating_vs_decmial_x/scaling.jsonl
moon run --release src/floating_vs_decmial_x/bench_common --target native \
  > artifacts/floating_vs_decmial_x/common_digits.jsonl

一つ目のコマンドは 1〜4,096 桁を、二つ目は 1、4、8、16、18、28 桁を計測します。どちらも JSONL の隣に HTML レポートを書き出します(bench のページを参照)。ホストを記録するには MARE_CPU、MARE_OS、MARE_BUILD_MODE などの MARE_* 変数を設定してください。未設定の事実は unknown または unspecified と書かれます。その後、図を描きます:

python3 tools/layout_x_decimal.py

レコードは計時範囲ごとに読んでください。arithmetic_only は公開演算一回、semantic_equivalent_pipeline は GDA で X のポリシーを再現する一連の処理全体です。異なるターゲットの結果を合わせることはしません。

さらに進んで

独自の入力の精度。 XCompatible の除算では、working_precision はオペランドの長さではなく桁数の差で決まります。その精度より長い入力はフィクスチャの構築時に丸められます(設計ページを参照)。一致を保証したいときは両オペランドを pp 桁以下にしてください。

速度比の解釈。 x_speedup_vs_gda は対応サンプルの中央値の比で、decision は対応差の中央値に 3 % の実用上のしきい値を適用します。どちらも信頼区間ではありません。式は兄弟ベンチマークの統計の節にあります。

兄弟パッケージ。 dzmingli_vs_floating は厳密なオラクル、解析を含む第二の計時範囲、19 演算で同じ手法を適用します。

よくある落とし穴

  • prepare_fixture と working_precision の既定値は XCompatible です。厳密なグループを使いたいときは semantics=ExactOverlap を明示してください。
  • XCompatible の除算では、有効桁数が精度より多いオペランドは GDA が割る前に丸められます。そのため X が 0.49999999999999999999999999990.4999999999999999999999999999 を返すところで GDA が 0.50.5 を返すことがあり、計時も短い GDA のオペランドと全長の X のオペランドを比べることになります。
  • x_from_neutral はスケールが 28 を超えると中断します。X はそのような値を保持できません。
  • HTML の要約行が正しいのは失敗のない実行だけです。代わりにレポートの validation_count と failed_count を読んでください。
  • 現在の moonbitlang/core では、wasm-gc の BigInt::from_string が長い文字列で誤った結果を返します。テスト division precision follows the requested semantic contract はこれで 9 を 4,096 個並べたオペランドを作り、その誤った解析から生じた値 4097 を期待しています。正しい作業精度は 4096+1+2=40994096 + 1 + 2 = 4099 です。そのためこのテストは wasm-gc では通り、native と js では失敗します。長いテスト値は parse_decimal_value か BigInt の算術で作ってください。
  • X について公開された数値は moonbitlang/x@0.4.46 で計測されました。モジュールは現在 0.5.5 に依存していますが、JSONL の実装レコードにはまだ 0.4.46 と書かれています。

次のステップ