floating_vs_decmial_x API

パッケージ Luna-Flow/diff_bench/floating_vs_decmial_x は、同一の十進入力に対して moonbitlang/x/decimal(X)と Luna-Flow/floating/decimal_gda@0.7.1(GDA)を比較し、両者を BigInt オラクルで検査して Mare Mark で計測します。再現のために GitHub リポジトリに置かれたベンチマーク用ハーネスであり、他のプロジェクトの実行時依存ではありません。関数は契約外の入力で中断します。

パッケージ名は歴史的な綴り decmial のままです。チュートリアルでは各項目の使い方を示し、設計ページでは精度契約を導出します。ソース:src/floating_vs_decmial_x/。

例ではパッケージを @floating_vs_decmial_x として、また moonbitlang/core/bigint をインポートします。

中立な十進モデル

これらの項目の定義は dzmingli_vs_floating と同じです。各ベンチマークが単独でビルドできるよう、二つのパッケージは別々のコピーを持ちます。

DecimalValue

DecimalValue は値 c⋅10−sc \cdot 10^{-s} を持つ、実装に依存しない十進数 (c,s)(c, s) です。

pub(all) struct DecimalValue {
  coefficient : @bigint.BigInt
  scale : Int
}

normalize

normalize はスケールが非負である範囲で係数の末尾の 0 を取り除きます。0 は (0,0)(0, 0) になります。

pub fn normalize(DecimalValue) -> DecimalValue

canonical_string

canonical_string は正規化した値を指数なしの位取り記法で表します。等しい数は等しい文字列になります。

pub fn canonical_string(DecimalValue) -> String

parse_decimal_value

parse_decimal_value は符号、小数点、指数を省略可能に含む有限の十進文字列を正規化された値に読み込みます。それ以外では中断します。

pub fn parse_decimal_value(String) -> DecimalValue

OracleResult

OracleResult は正規化された値とその正準文字列を保持します。

pub struct OracleResult {
  value : DecimalValue
  canonical : String
}

to_oracle_result

to_oracle_result は値を正規化し、その正準文字列と組にします。

pub fn to_oracle_result(DecimalValue) -> OracleResult
test "neutral decimal model" {
  let v = @floating_vs_decmial_x.parse_decimal_value("0.00120")
  inspect(v.coefficient, content="12")
  inspect(v.scale, content="4")
  inspect(@floating_vs_decmial_x.canonical_string(v), content="0.0012")
  let r = @floating_vs_decmial_x.to_oracle_result({ coefficient: -1200N, scale: 2 })
  inspect(r.canonical, content="-12")
}

厳密な算術

add

add は両オペランドを大きい方のスケールに揃えたうえで厳密な和を返します。

pub fn add(DecimalValue, DecimalValue) -> DecimalValue

subtract

subtract は同じ揃え方の後で厳密な差を返します。

pub fn subtract(DecimalValue, DecimalValue) -> DecimalValue

multiply

multiply は厳密な積を返します。

pub fn multiply(DecimalValue, DecimalValue) -> DecimalValue

compare

compare は -1、0、1 のいずれかを返します。

pub fn compare(DecimalValue, DecimalValue) -> Int
test "exact arithmetic" {
  let a = @floating_vs_decmial_x.parse_decimal_value("123.45")
  let b = @floating_vs_decmial_x.parse_decimal_value("-4.5")
  let show = @floating_vs_decmial_x.canonical_string
  inspect(show(@floating_vs_decmial_x.add(a, b)), content="118.95")
  inspect(show(@floating_vs_decmial_x.subtract(a, b)), content="127.95")
  inspect(show(@floating_vs_decmial_x.multiply(a, b)), content="-555.525")
  inspect(@floating_vs_decmial_x.compare(b, a), content="-1")
}

参照オラクル

オラクルは X の結果ポリシーを模倣します。小数部は最大 28 桁で、それを超える桁は 0 方向に切り捨てます。

oracle_multiply

oracle_multiply は厳密な積を返し、スケールが 28 を超えるときは小数 28 桁まで 0 方向に切り捨てます。

pub fn oracle_multiply(DecimalValue, DecimalValue) -> DecimalValue

oracle_divide

oracle_divide は小数 28 桁まで 0 方向に切り捨てた商を、整数除算だけで計算して返します。

pub fn oracle_divide(DecimalValue, DecimalValue) -> DecimalValue

k=28+sr−sℓk = 28 + s_r - s_\ell とすると、結果の係数は k≥0k \ge 0 のとき trunc⁡(cℓ⋅10k/cr)\operatorname{trunc}(c_\ell \cdot 10^{k} / c_r)、そうでなければ trunc⁡(trunc⁡(cℓ/10−k)/cr)\operatorname{trunc}(\operatorname{trunc}(c_\ell / 10^{-k}) / c_r) で、スケールは 28 です。除数が 0 なら中断します。dzmingli_vs_floating と異なり、循環小数になる商も扱えます。

oracle_operation

oracle_operation は上記の規則で Operation を評価します。Add、Subtract、Compare は厳密、Multiply と Divide は切り捨て、Parse と Format は恒等です。

pub fn oracle_operation(Operation, DecimalValue, DecimalValue) -> OracleResult

オラクルは DecimalSemantics に依存しません。一つのオラクルで両方のグループをまかなえる理由は設計ページにあります。

test "reference oracle" {
  let one = @floating_vs_decmial_x.parse_decimal_value("1")
  let three = @floating_vs_decmial_x.parse_decimal_value("3")
  inspect(
    @floating_vs_decmial_x.oracle_operation(Divide, one, three).canonical,
    content="0.3333333333333333333333333333",
  )
  let tiny : @floating_vs_decmial_x.DecimalValue = { coefficient: 1N, scale: 20 }
  let wide : @floating_vs_decmial_x.DecimalValue = { coefficient: 123456789N, scale: 10 }
  inspect(
    @floating_vs_decmial_x.canonical_string(@floating_vs_decmial_x.oracle_multiply(tiny, wide)),
    content="0.0000000000000000000001234567",
  )
}

演算、意味論、スイート

Operation

Operation はこのベンチマークの演算ファミリーを列挙します。

pub(all) enum Operation {
  Add
  Subtract
  Multiply
  Divide
  Compare
  Parse
  Format
}

Parse と Format はどちらの側でも準備済みの左オペランドを返し、実行ファイルはこれらを実行しません。

operation_name

operation_name はレコードで使うスネークケースの名前("divide" など)を返します。

pub fn operation_name(Operation) -> String

DecimalSemantics

DecimalSemantics は両実装が満たすべき意味論的な契約を選びます。

pub(all) enum DecimalSemantics {
  ExactOverlap
  XCompatible
}

ExactOverlap は両ライブラリが厳密な結果を表現できる入力を使うので、どちらの側でも丸めは起きません。XCompatible は multiply と divide の後に quantize を行うことで X の小数 28 桁切り捨てを GDA で再現し、この処理は計時パスの内側にあります。

decimal_semantics_name

decimal_semantics_name は "exact_overlap" または "x_compatible" を返します。

pub fn decimal_semantics_name(DecimalSemantics) -> String

OperandShape

OperandShape は生成されたケースにラベルを付けます。ラベルはオペランドに影響しません。

pub(all) enum OperandShape {
  Small
  Large
  Boundary
  Cancellation
  Repeating
}

TimingScope

TimingScope は、スイートのメタデータとして、計時される呼び出しに含まれる内容を表します。

pub(all) enum TimingScope {
  ConstructionAndOperation
  OperationOnly
}

Mare Mark ランナーはこれを読みません。比較レコードは代わりに文字列の範囲を持ちます。XCompatible での Multiply と Divide は "semantic_equivalent_pipeline"、それ以外は "arithmetic_only" です。

Suite

Suite はベンチマークスイートを記述するメタデータです。

pub struct Suite {
  name : String
  operations : Array[Operation]
  scales : Array[Int]
  timing_scope : TimingScope
  warmup : Int
  samples : Int
}

default_suite

default_suite はスイート "decimal-x-floating-gda" を返します。全 7 演算、スケール [0, 2, 6, 18, 28]、OperationOnly、ウォームアップ 5 回、サンプル 20 個です。

pub fn default_suite() -> Suite

suite_case_count

suite_case_count は 演算数 × スケール数 × 演算ごとのケース数 を返します。

pub fn suite_case_count(Suite, Int) -> Int
test "operations, semantics and suites" {
  inspect(@floating_vs_decmial_x.operation_name(Divide), content="divide")
  inspect(@floating_vs_decmial_x.decimal_semantics_name(XCompatible), content="x_compatible")
  let suite = @floating_vs_decmial_x.default_suite()
  inspect(@floating_vs_decmial_x.suite_case_count(suite, 2), content="70")
}

決定的なケース

generate_decimal

generate_decimal(seed, digits, scale, negative) は係数がちょうど digits 桁(各桁は 1..91..9)で、そのうち scale 桁が小数部である十進文字列を作ります。digits <= 0 または scale < 0 なら中断します。

pub fn generate_decimal(Int, Int, Int, Bool) -> String

BenchmarkCase

BenchmarkCase は生成されたケースで、その文字列がシリアライズの境界です。

pub struct BenchmarkCase {
  id : String
  operation : Operation
  shape : OperandShape
  left : String
  right : String
  scale : Int
}

generate_cases

generate_cases(seed, count, operation) は、桁数 1..241..24、スケール 0..80..8 の再現可能なケースを、時計やグローバルな乱数状態を使わずに count 個返します。

pub fn generate_cases(Int, Int, Operation) -> Array[BenchmarkCase]

serialize_case

serialize_case は decimal-neutral-v1 とケースの各フィールドを改行で連結します。

pub fn serialize_case(BenchmarkCase) -> String

fingerprint_case

fingerprint_case は Mare Mark の stable_fingerprint で serialize_case をハッシュします。

pub fn fingerprint_case(BenchmarkCase) -> String

expand_digit_scales

expand_digit_scales(sizes, n) は各係数サイズを n 回繰り返し、繰り返しごとに Mare Mark のデータセットを一つ作ります。

pub fn expand_digit_scales(Array[Int], Int) -> Array[Int]
test "deterministic cases" {
  inspect(@floating_vs_decmial_x.generate_decimal(0, 2, 4, true), content="-0.0018")
  let first = @floating_vs_decmial_x.generate_cases(17, 4, Multiply)
  let again = @floating_vs_decmial_x.generate_cases(17, 4, Multiply)
  inspect(first[3].right == again[3].right, content="true")
  assert_eq(@floating_vs_decmial_x.expand_digit_scales([1, 4], 3), [1, 1, 1, 4, 4, 4])
}

フィクスチャとアダプター

working_precision

working_precision(operation, left, right, semantics?) はフィクスチャの GDA コンテキスト精度を返します。semantics の既定値は XCompatible です。

pub fn working_precision(Operation, DecimalValue, DecimalValue, semantics? : DecimalSemantics) -> Int

dℓ,drd_\ell, d_r を係数の桁数、sℓ,srs_\ell, s_r をスケールとすると:

演算精度
Add, Subtractmax⁡(dℓ,dr)+∣sℓ−sr∣+2\max(d_\ell, d_r) + \lvert s_\ell - s_r \rvert + 2
Multiplydℓ+dr+1d_\ell + d_r + 1
Divide, ExactOverlapdℓ+dr+2d_\ell + d_r + 2
Divide, XCompatiblemax⁡(1, dℓ−sℓ−dr+sr+1)+30\max(1,\ d_\ell - s_\ell - d_r + s_r + 1) + 30
Compare, Parse, Formatmax⁡(dℓ,dr)+1\max(d_\ell, d_r) + 1

各行とそれがカバーするオペランドクラスは設計ページで導出しています。

DecimalFixture

DecimalFixture は計時前に構築した両実装のオペランドを保持します。

pub struct DecimalFixture {
  neutral_left : DecimalValue
  neutral_right : DecimalValue
  operation : Operation
  semantics : DecimalSemantics
  x_left : @decimal.Decimal
  x_right : @decimal.Decimal
  gda_left : @decimal_gda.Decimal
  gda_right : @decimal_gda.Decimal
  gda_quantum_28 : @decimal_gda.Decimal
  gda_context : @decimal_gda.GdaContext
}

@decimal は moonbitlang/x/decimal です。gda_quantum_28 は 10−2810^{-28} で、XCompatible の後処理で使う量子です。

prepare_fixture

prepare_fixture(operation, left, right, semantics?) は精度 pp を計算し、両オペランドを X 用と GDA 用に変換し、精度 pp と 0 方向への丸めを持つ GDA コンテキストを作ります。

pub fn prepare_fixture(Operation, DecimalValue, DecimalValue, semantics? : DecimalSemantics) -> DecimalFixture

semantics の既定値は XCompatible です。GDA のオペランドは精度 pp と偶数丸めで解析されるため、有効桁数が pp を超えるオペランドは丸められます。既知の制限を参照してください。

x_from_neutral

x_from_neutral は Decimal::new で値を変換します。スケールが X の範囲 0..280..28 を外れると中断します。

pub fn x_from_neutral(DecimalValue) -> @decimal.Decimal

gda_from_neutral

gda_from_neutral(value, precision) は Decimal::from_string(precision~) で正準文字列を解析します。

pub fn gda_from_neutral(DecimalValue, Int) -> @decimal_gda.Decimal

DecimalObservation

DecimalObservation はアダプター呼び出し一回の生の結果です。

pub(all) enum DecimalObservation {
  X(@decimal.Decimal)
  XCompare(Int)
  Gda(@decimal_gda.GdaOutcome[@decimal_gda.Decimal])
}

XCompare は -1、0、1 に正規化済みの X の比較結果を保持します。

run_x

run_x は X の演算子 +、-、*、/ または Compare::compare でフィクスチャの演算を実行します。

pub fn run_x(DecimalFixture) -> DecimalObservation

run_gda

run_gda は floating GDA でフィクスチャの演算を実行します。XCompatible では、スケールが 28 を超える積とすべての商を 10−2810^{-28} に量子化します。

pub fn run_gda(DecimalFixture) -> DecimalObservation

canonical_x

canonical_x は X の十進数を、その係数とスケールから正規化された DecimalValue に変換します。

pub fn canonical_x(@decimal.Decimal) -> DecimalValue

canonical_observation

canonical_observation は任意の観測値を正規化された DecimalValue に変換します。GDA の結果は to_string と parse_decimal_value を通ります。

pub fn canonical_observation(DecimalObservation) -> DecimalValue
test "fixtures and adapters" {
  let one = @floating_vs_decmial_x.parse_decimal_value("1")
  let three = @floating_vs_decmial_x.parse_decimal_value("3")
  inspect(@floating_vs_decmial_x.working_precision(Divide, one, three), content="31")
  let fixture = @floating_vs_decmial_x.prepare_fixture(Divide, one, three)
  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")
}

Mare Mark との統合

MareDecimalInput

MareDecimalInput は一つの Mare Mark データセットの具体化された入力です。

pub(all) struct MareDecimalInput {
  left : DecimalValue
  right : DecimalValue
  operation : Operation
  semantics : DecimalSemantics
  digits : Int
  left_scale : Int
  right_scale : Int
}

PerformanceResult

PerformanceResult は対応のある確認用サンプルから、一つの演算、意味論グループ、係数サイズの結果を要約します。

pub(all) struct PerformanceResult {
  operation : Operation
  semantics : DecimalSemantics
  timing_scope : String
  digits : Int
  x_median_us : Double
  gda_median_us : Double
  gda_relative_delta_pct : Double
  x_speedup_vs_gda : Double
  decision : String
  samples : Int
}

x_speedup_vs_gda は GDA の中央値を X の中央値で割った値です(11 を超えれば X が速い)。decision は "gda_faster"、"x_faster"、"equivalent"、"invalid"、"unknown" のいずれかです。dzmingli_vs_floating と異なり結果は常に対応付けられ、検証の失敗はレポートの件数にだけ現れます。

PerformanceResult::to_json

PerformanceResult::to_json は成果物バージョン mmka_1 の "comparison" JSONL レコードを出力します。

pub fn PerformanceResult::to_json(Self) -> String

MareBenchmarkReport

MareBenchmarkReport は一回の実行の結果、生の JSONL、検証件数をまとめます。validation_count は合格と不合格の両方の検証を数えます。

pub(all) struct MareBenchmarkReport {
  results : Array[PerformanceResult]
  jsonl : String
  validation_count : Int
  failed_count : Int
}

smoke_protocol

smoke_protocol は短いテスト用プロトコルを返します。ウォームアップ 2 回、0.5 ms の較正バッチ、確認用の繰り返し 3 回です。

pub fn smoke_protocol() -> @model.RunProtocol

default_benchmark_protocol

default_benchmark_protocol は公開された実行のプロトコルを返します。ウォームアップ 5 回、5 ms の較正バッチ、シード 0xDEC1A1 のバランスブロック、外れ値は報告のみ、確認用の繰り返し 20 回です。

pub fn default_benchmark_protocol() -> @model.RunProtocol

benchmark_environment

benchmark_environment は MARE_* 環境変数から環境スナップショットを作り、未設定のものには固定の既定値を使います。

pub fn benchmark_environment() -> @model.EnvironmentSnapshot

run_mare_benchmark

run_mare_benchmark(operations, semantics, digit_scales, protocol, seed) は一つの意味論グループのもとで演算ごとに Mare Mark の実験を実行し、まとめたレポートを返します。

pub async fn run_mare_benchmark(Array[Operation], DecimalSemantics, Array[Int], @model.RunProtocol, UInt64) -> MareBenchmarkReport

各データセットは計時前にオラクルで検証されます。この関数は失敗で中断しないので、呼び出し側が failed_count を確認します。native と js ターゲットで動きます。

async test "mare mark smoke run" {
  let report = @floating_vs_decmial_x.run_mare_benchmark(
    [Divide],
    XCompatible,
    [16],
    @floating_vs_decmial_x.smoke_protocol(),
    42UL,
  )
  inspect(report.failed_count, content="0")
  inspect(report.results[0].timing_scope, content="semantic_equivalent_pipeline")
}

レポート

mare_performance_report_document

mare_performance_report_document(results, target, run_id) は Plot IR 文書を作ります。演算と意味論グループごとにレイテンシのプロットを一つ、グループごとに X 対 GDA の速度比プロットを一つ含みます。

pub fn mare_performance_report_document(Array[PerformanceResult], String, String, validation_count? : Int, failed_count? : Int) -> @ir_model.PlotDocument

コーパスの要約は、総数 validation_count + failed_count、合格 validation_count、不合格 failed_count です。validation_count はすでに失敗を含むので、要約が正しいのは failed_count が 0 のときだけです。設計ページを参照してください。

mare_performance_report_html

mare_performance_report_html は同じ文書を自己完結した HTML ページとして描画します。

pub fn mare_performance_report_html(Array[PerformanceResult], String, String, validation_count? : Int, failed_count? : Int) -> String
test "performance report" {
  let result : @floating_vs_decmial_x.PerformanceResult = {
    operation: Add,
    semantics: ExactOverlap,
    timing_scope: "arithmetic_only",
    digits: 16,
    x_median_us: 1.0,
    gda_median_us: 2.0,
    gda_relative_delta_pct: 100.0,
    x_speedup_vs_gda: 2.0,
    decision: "x_faster",
    samples: 60,
  }
  let html = @floating_vs_decmial_x.mare_performance_report_html(
    [result],
    "native",
    "example",
    validation_count=2,
  )
  inspect(html.contains("x_decimal"), content="true")
}