dzmingli_vs_floating API

パッケージ Luna-Flow/diff_bench/dzmingli_vs_floating は、同一の十進入力に対して DzmingLi/decimal@0.2.2 と Luna-Flow/floating/decimal_gda@0.7.1 を比較し、両者を厳密な BigInt オラクルで検査し、Mare Mark で計測します。これは十進ライブラリではなくベンチマーク用のハーネスです。無効なフィクスチャはハーネス自身のバグであるため、どの関数も契約外の入力に対しては Result を返さずに中断します。

チュートリアルでは各項目の使い方を示し、設計ページでは精度契約とオラクルを導出します。ソース:src/dzmingli_vs_floating/。

このページの例はブラックボックステストで、パッケージを @dzmingli_vs_floating として、また moonbitlang/core/bigint をインポートします。

中立な十進モデル

DecimalValue

DecimalValue は実装に依存しない十進数で、すべてのフィクスチャ、オラクルの結果、観測値はこれに変換されます。

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

値 (c,s)(c, s) は c⋅10−sc \cdot 10^{-s} を表します。符号は係数が持ちます。このパッケージが生成する値は s≥0s \ge 0 を満たし、負の指数は parse_decimal_value が係数に掛け込みます。(1200,2)(1200, 2) と (12,0)(12, 0) のように多くの組が同じ数を表すため、normalize がその中の一つを選びます。

normalize

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

pub fn normalize(DecimalValue) -> DecimalValue

0 は (0,0)(0, 0) になります。それ以外の値では、結果は s=0s = 0 であるか、係数が 1010 で割り切れません。この形は数ごとに一意なので、二つの値が等しいのは正規化形が等しいときに限ります(証明は設計ページ)。コスト:取り除く 0 一つにつき BigInt の除算 1 回。

canonical_string

canonical_string は正規化した値を指数なしの通常の位取り記法で表します。

pub fn canonical_string(DecimalValue) -> String

この文字列は負の値では先頭に - を持ち、∣v∣<1|v| < 1 のときは 0. の接頭辞と小数部の先頭の 0 を持ち、末尾の 0 は持ちません。数に対して単射なので、文字列の等価は値の等価です。この文字列がパッケージ内のすべての検証における比較の境界です。

parse_decimal_value

parse_decimal_value は有限の十進文字列を正規化された DecimalValue に読み込みます。

pub fn parse_decimal_value(String) -> DecimalValue

受け付ける構文:省略可能な先頭の符号、. を高々一つ含む数字列、そして独自の符号を持てる省略可能な指数 e または E。数字は一桁ずつ BigInt に累積されるので、パーサーは BigInt::from_string に依存しません。空の数字列、数字のない指数、NaN、Infinity、その他の文字では中断します。指数は Int で保持されます。

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 = @dzmingli_vs_floating.parse_decimal_value("-12.3400e1")
  inspect(v.coefficient, content="-1234")
  inspect(v.scale, content="1")
  let wide : @dzmingli_vs_floating.DecimalValue = { coefficient: 1200N, scale: 2 }
  inspect(@dzmingli_vs_floating.normalize(wide).scale, content="0")
  inspect(@dzmingli_vs_floating.canonical_string(v), content="-123.4")
  inspect(@dzmingli_vs_floating.to_oracle_result(wide).canonical, content="12")
}

厳密な算術

これらの関数は DecimalValue 上で厳密に計算します。精度も丸めもありません。結果は正規化されます。

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 = @dzmingli_vs_floating.parse_decimal_value("1.25")
  let b = @dzmingli_vs_floating.parse_decimal_value("-0.005")
  let show = @dzmingli_vs_floating.canonical_string
  inspect(show(@dzmingli_vs_floating.add(a, b)), content="1.245")
  inspect(show(@dzmingli_vs_floating.subtract(a, b)), content="1.255")
  inspect(show(@dzmingli_vs_floating.multiply(a, b)), content="-0.00625")
  inspect(@dzmingli_vs_floating.compare(a, b), content="1")
}

参照オラクル

オラクルはベンチマーク対象のすべての演算の期待値を、BigInt 算術だけで厳密に計算します。その意味論は十分な精度と 0 方向への丸めを持つ GDA コンテキストのものです。各規則は設計ページにあります。

oracle_multiply

oracle_multiply は厳密な積を返します。オラクル用の名前を付けた multiply です。

pub fn oracle_multiply(DecimalValue, DecimalValue) -> DecimalValue

oracle_divide

oracle_divide は商が有限の十進展開を持つとき、厳密な商を返します。

pub fn oracle_divide(DecimalValue, DecimalValue) -> DecimalValue

除数が 0 のとき、また約分後の分母が 22 と 55 以外の素因数を持つとき(“repeating decimal in exact decimal oracle”)に中断します。コスト:BigInt の GCD 1 回と、結果の小数桁ごとに 1010 倍 1 回。

oracle_operation

oracle_operation は二項または単項の Operation を評価し、正規化された結果を返します。

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

第三オペランドを 0 とした oracle_operation3 と同じなので、加数が 0 でない限り Fma に対しては誤った答えを返します。

oracle_operation3

oracle_operation3 は任意の Operation を評価します。第三オペランドは Fma の加数で、それ以外では無視されます。

pub fn oracle_operation3(Operation, DecimalValue, DecimalValue, DecimalValue) -> OracleResult
演算期待値
Add, Subtract, Multiply厳密な結果
Divide厳密な有限の商。そうでなければ中断
DivideInteger0 方向に切り捨てた商
Remaindera−b⋅trunc⁡(a/b)a - b \cdot \operatorname{trunc}(a/b)、符号は aa と同じ
Powerana^n を繰り返し乗算で計算。nn は非負整数でなければならない
Fma厳密な a⋅b+ca \cdot b + c
SquareRootスケールが偶数の完全平方数の厳密な平方根。そうでなければ中断
Plus, Minus, Absaa, −a-a, ∣a∣\lvert a \rvert
Quantizeaa を bb のスケールまで 0 方向に切り捨て
Rescaleaa をスケール −b-b まで 0 方向に切り捨て。bb は整数でなければならない
ScaleBa⋅10ba \cdot 10^{b}。bb は整数でなければならない
Reduce正規化した aa
ToIntegralExact, ToIntegralValueaa を 0 方向に整数へ切り捨て
Compare十進数としての -1、0、1
Parse, Formataa をそのまま
test "reference oracle" {
  let a = @dzmingli_vs_floating.parse_decimal_value("1.25")
  let b = @dzmingli_vs_floating.parse_decimal_value("8")
  let show = @dzmingli_vs_floating.canonical_string
  inspect(show(@dzmingli_vs_floating.oracle_divide(a, b)), content="0.15625")
  let minus_seven_and_a_half = @dzmingli_vs_floating.parse_decimal_value("-7.5")
  let two = @dzmingli_vs_floating.parse_decimal_value("2")
  let r = @dzmingli_vs_floating.oracle_operation(Remainder, minus_seven_and_a_half, two)
  inspect(r.canonical, content="-1.5")
  let fma = @dzmingli_vs_floating.oracle_operation3(Fma, a, b, two)
  inspect(fma.canonical, content="12")
}

演算とスイート

Operation

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

pub(all) enum Operation {
  Add
  Subtract
  Multiply
  Divide
  DivideInteger
  Remainder
  Power
  Fma
  SquareRoot
  Plus
  Minus
  Abs
  Quantize
  Rescale
  ScaleB
  Reduce
  ToIntegralExact
  ToIntegralValue
  Compare
  Parse
  Format
}

Parse と Format は恒等パスです。どちらのアダプターも準備済みの左オペランドを返すだけなので何も計測せず、実行ファイルもこれらを実行しません。

operation_name

operation_name は JSONL レコードとフィンガープリントで使う安定したスネークケースの名前("divide_integer" や "sqrt" など)を返します。

pub fn operation_name(Operation) -> String

OperandShape

OperandShape は生成されたケースにラベルを付けます。

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

ラベルはケースとそのフィンガープリントにシリアライズされます。generate_cases はインデックスに従って循環的に割り当て、オペランドの生成方法には影響しません。

TimingScope

TimingScope は計時される一回の呼び出しに何を含めるかを選びます。

pub(all) enum TimingScope {
  OperationOnly
  FullPath
}

OperationOnly は計時前に準備したオペランドに対する公開演算を計時します。FullPath は加えて、準備済みのコンテキストで二つの正準オペランド文字列(Fma では加数も)を解析します。

timing_scope_name

timing_scope_name は "arithmetic_only" または "full_path" を返します。

pub fn timing_scope_name(TimingScope) -> String

Suite

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

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

default_suite

default_suite は "dzmingli-vs-floating-gda" という名前のスイートを返します。全 21 演算、スケール [0, 2, 6, 18, 28]、OperationOnly、ウォームアップ 5 回、サンプル 20 個です。Mare Mark ランナーはこれを読まず、プロトコルは default_benchmark_protocol から取ります。

pub fn default_suite() -> Suite

suite_case_count

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

pub fn suite_case_count(Suite, Int) -> Int
test "operations and suites" {
  inspect(@dzmingli_vs_floating.operation_name(SquareRoot), content="sqrt")
  inspect(@dzmingli_vs_floating.timing_scope_name(FullPath), content="full_path")
  let suite = @dzmingli_vs_floating.default_suite()
  inspect(@dzmingli_vs_floating.suite_case_count(suite, 4), content="420")
}

決定的なケース

generate_decimal

generate_decimal(seed, digits, scale, negative) は係数がちょうど digits 桁で、そのうち scale 桁が小数点以下にある十進文字列を作ります。

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

ii 桁目の数字は 1+((seed+7i) mod 9)1 + ((\mathit{seed} + 7i) \bmod 9) なので、すべての桁は 1..91..9 に入ります。係数はちょうど digits 桁で末尾に 0 を持ちません。scale >= digits のとき文字列は 0. と先頭の 0 で始まります。digits <= 0 または scale < 0 なら中断します。

BenchmarkCase

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

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

generate_cases

generate_cases(seed, count, operation) は時計もグローバルな乱数状態も読まずに、再現可能なケースを count 個返します。

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

ケース ii の両オペランドは 1+((seed+11i) mod 24)1 + ((\mathit{seed} + 11i) \bmod 24) 桁、スケール (seed+5i) mod 9(\mathit{seed} + 5i) \bmod 9 です。除数は選別されていないので Divide のケースは有限小数にならないことがあります。oracle_divide ではなくアダプターで使ってください。

serialize_case

serialize_case はバージョンタグ decimal-neutral-v1、id、演算名、形状名、両オペランド、スケールを改行で連結します。

pub fn serialize_case(BenchmarkCase) -> String

fingerprint_case

fingerprint_case は Mare Mark の stable_fingerprint で serialize_case をハッシュし、sha256: で始まる文字列を返します。

pub fn fingerprint_case(BenchmarkCase) -> String

expand_digit_scales

expand_digit_scales(sizes, n) は各係数サイズを n 回繰り返し、Mare Mark がサイズごとに n 個の独立したデータセットを作るようにします。

pub fn expand_digit_scales(Array[Int], Int) -> Array[Int]
test "deterministic cases" {
  inspect(@dzmingli_vs_floating.generate_decimal(3, 6, 2, true), content="-4297.53")
  let cases = @dzmingli_vs_floating.generate_cases(7, 3, Add)
  inspect(cases[1].left, content="-9753186429753186.429")
  inspect(
    @dzmingli_vs_floating.fingerprint_case(cases[0]) ==
    @dzmingli_vs_floating.fingerprint_case(@dzmingli_vs_floating.generate_cases(7, 3, Add)[0]),
    content="true",
  )
  assert_eq(@dzmingli_vs_floating.expand_digit_scales([4, 16], 2), [4, 4, 16, 16])
}

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

working_precision

working_precision は、与えられたオペランドに対する演算の厳密な結果を保持できるだけの有効桁数を返します。

pub fn working_precision(Operation, DecimalValue, DecimalValue) -> 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
Multiply, Fma, Power, Divide, DivideInteger, Remainderdℓ+dr+2d_\ell + d_r + 2
SquareRoot と単項演算dℓ+2d_\ell + 2
Compare, Parse, Formatmax⁡(dℓ,dr)+1\max(d_\ell, d_r) + 1

prepare_fixture3 はガード桁を 2 桁加え、Power と Fma は別の式で上書きします。各上界がどのオペランドクラスをカバーするかは設計ページで証明しています。

DecimalFixture

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

pub struct DecimalFixture {
  left_text : String
  right_text : String
  third_text : String
  operation : Operation
  dz_left : @decimal.Decimal
  dz_right : @decimal.Decimal
  dz_third : @decimal.Decimal
  dz_context : @decimal.Context
  gda_left : @decimal_gda.Decimal
  gda_right : @decimal_gda.Decimal
  gda_third : @decimal_gda.Decimal
  gda_context : @decimal_gda.GdaContext
}

@decimal は DzmingLi/decimal、@decimal_gda は Luna-Flow/floating/decimal_gda です。*_text フィールドは FullPath が改めて解析する正準文字列です。

prepare_fixture3

prepare_fixture3(operation, left, right, third) は精度 pp を選び、精度 pp と 0 方向への丸めで両方のコンテキストを作り、三つの正準文字列を両ライブラリに解析させます。

pub fn prepare_fixture3(Operation, DecimalValue, DecimalValue, DecimalValue) -> DecimalFixture

pp は Power では 2dℓ+42 d_\ell + 4、Fma では dℓ+dr+dt+∣sℓ+sr−st∣+4d_\ell + d_r + d_t + \lvert s_\ell + s_r - s_t \rvert + 4、それ以外では working_precision(...) + 2 です。DzmingLi が変換構文エラーを報告すると中断します。

prepare_fixture

prepare_fixture(operation, left, right) は第三オペランドを 0 とした prepare_fixture3 です。

pub fn prepare_fixture(Operation, DecimalValue, DecimalValue) -> DecimalFixture

dz_from_neutral

dz_from_neutral は Context::exact() のもとで値を DzmingLi の十進数に変換します。

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

gda_from_neutral

gda_from_neutral(value, precision) は、指定精度と 0 方向への丸めを持つコンテキストで正準文字列を GDA の十進数に解析します。

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

DecimalObservation

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

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

GDA のバリアントはフラグや次のコンテキストを含む結果全体を保持します。検証が読むのはその値だけです。

run_dz

run_dz は準備済みのオペランドに対して DzmingLi でフィクスチャの演算を実行します。これが OperationOnly で計時される本体です。

pub fn run_dz(DecimalFixture) -> DecimalObservation

run_dz_full

run_dz_full はフィクスチャの三つの文字列を dz_context で解析してから演算を実行します。これが FullPath で計時される本体です。

pub fn run_dz_full(DecimalFixture) -> DecimalObservation

run_gda

run_gda は準備済みのオペランドに対して floating GDA でフィクスチャの演算を実行します。

pub fn run_gda(DecimalFixture) -> DecimalObservation

run_gda_full

run_gda_full はフィクスチャの三つの文字列を gda_context で解析してから演算を実行します。

pub fn run_gda_full(DecimalFixture) -> DecimalObservation

canonical_observation

canonical_observation は観測値を正規化された DecimalValue に変換します。

pub fn canonical_observation(DecimalObservation) -> DecimalValue

DzmingLi の結果は to_sci_string、GDA の結果は to_string を経て、どちらも parse_decimal_value を通ります。指数、末尾の 0、ステータスフラグは捨てられます。NaN や無限大の結果では中断します。

test "fixtures and adapters" {
  let a = @dzmingli_vs_floating.parse_decimal_value("1.25")
  let b = @dzmingli_vs_floating.parse_decimal_value("8")
  inspect(@dzmingli_vs_floating.working_precision(Divide, a, b), content="6")
  let fixture = @dzmingli_vs_floating.prepare_fixture(Divide, a, b)
  inspect(fixture.gda_context.precision(), content="8")
  let show = (o : @dzmingli_vs_floating.DecimalObservation) => {
    @dzmingli_vs_floating.canonical_string(
      @dzmingli_vs_floating.canonical_observation(o),
    )
  }
  inspect(show(@dzmingli_vs_floating.run_dz(fixture)), content="0.15625")
  inspect(show(@dzmingli_vs_floating.run_gda(fixture)), content="0.15625")
  inspect(show(@dzmingli_vs_floating.run_gda_full(fixture)), content="0.15625")
}

Mare Mark との統合

MareDecimalInput

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

pub(all) struct MareDecimalInput {
  left : DecimalValue
  right : DecimalValue
  third : DecimalValue
  operation : Operation
  timing_scope : TimingScope
  digits : Int
  left_scale : Int
  right_scale : Int
}

PerformanceResult

PerformanceResult は一つの演算、計時範囲、係数サイズの結果を要約します。

pub(all) struct PerformanceResult {
  operation : Operation
  timing_scope : TimingScope
  digits : Int
  correctness_valid : Bool
  dz_median_us : Double
  gda_median_us : Double
  gda_relative_delta_pct : Double
  dz_speedup_vs_gda : Double
  decision : String
  samples : Int
}

correctness_valid は、そのサイズで両実装のすべての検証が通ったとき真になります。そのとき中央値は対応のある確認用サンプルから求められ、dz_speedup_vs_gda は GDA の中央値を DzmingLi の中央値で割った値(11 を超えれば DzmingLi が速い)、gda_relative_delta_pct は対応差の中央値を DzmingLi の中央値に対する割合で表したもの、decision は "gda_faster"、"dzmingli_faster"、"equivalent"、"invalid"、"unknown" のいずれかです。そうでない場合、中央値は各実装自身の有効サンプルから求め、速度比と差は 0.0、decision は "invalid_correctness" になります。

PerformanceResult::to_json

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

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

MareBenchmarkReport

MareBenchmarkReport は一回の実行の結果、生の JSONL、検証件数をまとめます。

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

validation_count は合否にかかわらず、すべてのオラクル検証を数えます。

smoke_protocol

smoke_protocol はテスト用の短い Mare Mark プロトコルを返します。ウォームアップ 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_* 環境変数から JSONL に記録する環境スナップショットを作ります。

pub fn benchmark_environment() -> @model.EnvironmentSnapshot

MARE_TARGET、MARE_COMPILER、MARE_BUILD_MODE、MARE_CPU、MARE_DEVICE、MARE_FREQUENCY_POLICY、MARE_OS、MARE_EXECUTION_CONTEXT、MARE_REPOSITORY_STATE を読みます。未設定または空の変数は unknown、unspecified などの固定の既定値になり、ホストからは何も検出しません。

run_mare_benchmark

run_mare_benchmark(operations, scope, digit_scales, protocol, seed) は演算ごとに Mare Mark の実験を一回実行し、まとめたレポートを返します。

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

digit_scales の各要素が一つのデータセットです。サイズを繰り返すには expand_digit_scales を使います。各データセットは計時前にオラクルで検証されます。この関数は検証失敗で中断しないので、呼び出し側が failed_count を確認します。非同期ランタイムが必要なため、native と js ターゲットで動きます。

async test "mare mark smoke run" {
  let report = @dzmingli_vs_floating.run_mare_benchmark(
    [Add],
    OperationOnly,
    [4],
    @dzmingli_vs_floating.smoke_protocol(),
    42UL,
  )
  inspect(report.failed_count, content="0")
  inspect(report.validation_count, content="2")
  inspect(report.results[0].correctness_valid, content="true")
  inspect(report.results[0].to_json().contains("\"type\":\"comparison\""), content="true")
}

レポート

mare_performance_report_document

mare_performance_report_document(results, target, run_id) は Mare Mark の Plot IR 文書を作ります。演算と計時範囲ごとにレイテンシのプロットを一つ、計時範囲ごとに DzmingLi 対 GDA の速度比プロットを一つ含みます。

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

correctness_valid が偽のサイズでは GDA のレイテンシ点は残りますが、DzmingLi の点と速度比の点はありません。コーパスの要約は、総数 validation_count、合格 validation_count - failed_count、不合格 failed_count です。

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 : @dzmingli_vs_floating.PerformanceResult = {
    operation: Add,
    timing_scope: OperationOnly,
    digits: 16,
    correctness_valid: true,
    dz_median_us: 1.0,
    gda_median_us: 2.0,
    gda_relative_delta_pct: 100.0,
    dz_speedup_vs_gda: 2.0,
    decision: "dzmingli_faster",
    samples: 60,
  }
  let html = @dzmingli_vs_floating.mare_performance_report_html(
    [result],
    "native",
    "example",
    validation_count=2,
  )
  inspect(html.contains("Total 2 · passed 2 · failed 0"), content="true")
}