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 接受第三个操作数;其他运算忽略它。下面的循环在适合全部运算的输入上检查 16 种运算:SquareRoot 用完全平方数,Power 用整数指数,ScaleB 用零移位。

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")
}

看看精度不足是什么样子

夹具精度的选择保证没有结果被舍入。如果它太小,向零舍入会缩短结果,预言机比较就会失败。这里用 floating 的 decimal_gda 包(以 @decimal_gda 导入),在四位精度的上下文中计算一个有六位有效数字的积:

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 个数据集 × 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 位起校验失败。在读取数字之前,确认每条 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 失败仍然存在,它就以非零状态退出。

进阶

你自己的操作数类别。 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 传入的加数为零,因此对 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 中,它对数千位的字符串返回错误的值(4,096 个 9 被解析为一个 4,094 位的数)。parse_decimal_value 自己累加数字,不受影响。包内测试 division precision covers exact terminating quotients 使用了 from_string,但只断言一个下界,因此在所有目标上都能通过。
  • DzmingLi 在乘积超过约 21,475 位时中止(见设计页);乘法输入请保持在 10,000 位以内。

下一步