数值语义

本指南确立手册其余各页共用的数值术语。它区分五个容易混淆的概念:值的存储表示、运算所定义的精确值、上下文返回的舍入结果、舍入引发的状态,以及区间所保证的包络。每一节先给出定义,再推导库所依赖的性质,并配以可运行的示例。

值与表示

有限二进制值是一个带符号的整数系数乘以二的幂,有限十进制值则是乘以十的幂:

x=(−1)s⋅c⋅2eorx=(−1)s⋅c⋅10q,s∈{0,1}, c∈N, e,q∈Z.x = (-1)^s \cdot c \cdot 2^{e} \qquad\text{or}\qquad x = (-1)^s \cdot c \cdot 10^{q}, \qquad s \in \{0, 1\},\ c \in \mathbb{N},\ e, q \in \mathbb{Z}.

三元组 (s,c,e)(s, c, e) 是表示;实数 xx 是值。多个表示可以对应同一个值:3⋅2−13 \cdot 2^{-1} 与 6⋅2−26 \cdot 2^{-2} 都是 1.51.5。两种基数以不同方式处理这种冗余。

  • BinFloat 在构造有限值时总会去掉系数中末尾的二的因子,因此有限非零 BinFloat 的系数总是奇数。其打印形式 c p e 展示的正是这一规范对。
  • Decimal(位于 decimal 与 decimal_gda 中)保留给定的指数。指数 qq 称为量子(quantum),同一个值的全部表示构成它的同值类(cohort):12.3400、12.340 与 12.34 是同一个值的三个同值类成员。

每个值还带有一个精度 pp:舍入结果可使用的基数位数。精度是元数据和舍入上界;它并不表示每一个存储的数位都是有效数字,也从不改变基数。

///|
test "representation versus value" {
  // 6 * 2^-2 is stored as the canonical 3 * 2^-1.
  let three_halves = @bin_float.BinFloat::make(
    @bin_float.BinCoeff::from_uint64(6UL),
    -2,
    53,
  )
  inspect(three_halves, content="3p-1")
  inspect(three_halves.to_shortest_string(), content="1.5")
  // A decimal keeps its quantum until it is normalized explicitly.
  let price = @decimal.Decimal::from_string("12.3400").unwrap()
  inspect(price.quantum(), content="-4")
  inspect(price.normalized(), content="12.34")
  inspect(price.normalized().quantum(), content="-2")
  inspect(price.compare(price.normalized()), content="0")
  inspect(price.same_quantum(price.normalized()), content="false")
}

有限值之外还有特殊值:带符号的无穷 ±∞\pm\infty,以及分为静默与信号两种的 NaN(“not a number”,非数),每个 NaN 都带有符号和载荷(payload)。classify 报告 Finite、Infinity 或 NaN;@def.is_finite、@def.is_nan、@def.is_infinite 与 @def.is_zero 适用于所有实现了 @def.Floating 的类型。

精确值与舍入结果

每个算术运算 ∘\circ 首先定义一个精确值:无误差计算得到的实数 x∘yx \circ y(或 x\sqrt{x}、exp⁡x\exp x 等)。格式很少能恰好容纳它,因此上下文将其映射为一个可表示的舍入结果

fl⁡(x∘y)=rnd⁡(x∘y),\operatorname{fl}(x \circ y) = \operatorname{rnd}(x \circ y),

其中 rnd⁡\operatorname{rnd} 是上下文的舍入函数。若运算结果恰好如此——即精确值只舍入一次——则称该运算是正确舍入的。floating 中的每个算术运算、平方根、融合乘加、求余、转换与解析都是正确舍入的;初等函数借助认证细化同样是正确舍入的(参见架构)。

只舍入一次至关重要。融合乘加 fma_ctx 对 xy+zxy + z 只舍入一次,而先 mul_ctx 再 add_ctx 则舍入两次。在 binary64 中取 x=fl⁡(0.1)x = \operatorname{fl}(0.1),10x10x 并不恰好等于 11,只有融合形式能看出差别:

///|
test "one rounding versus two" {
  let ctx = @bin_float.BinaryContext::binary64()
  let tenth = @bin_float.BinFloat::from_double(0.1)
  let ten = @bin_float.BinFloat::from_int(10)
  let minus_one = @bin_float.BinFloat::from_int(-1)
  let (fused, _) = tenth.fma_ctx(ten, minus_one, ctx)
  inspect(fused, content="1p-54")
  let (product, _) = tenth.mul_ctx(ten, ctx)
  inspect(product, content="1p0")
}

融合结果 2−542^{-54} 正是舍入后乘积的精确误差:乘积 10⋅fl⁡(0.1)10 \cdot \operatorname{fl}(0.1) 比 11 大 2−542^{-54},而 mul_ctx 把这部分超出量舍掉了。

舍入函数

设 F\mathbb{F} 为上下文可表示的值集合,并添加 ±∞\pm\infty。对实数 xx,定向舍入为

RD⁡(x)=max⁡{ y∈F:y≤x },RU⁡(x)=min⁡{ y∈F:y≥x },RZ⁡(x)={RD⁡(x)x≥0RU⁡(x)x<0,RA⁡(x)={RU⁡(x)x≥0RD⁡(x)x<0,\begin{aligned} \operatorname{RD}(x) &= \max\{\, y \in \mathbb{F} : y \le x \,\}, & \operatorname{RU}(x) &= \min\{\, y \in \mathbb{F} : y \ge x \,\}, \\ \operatorname{RZ}(x) &= \begin{cases} \operatorname{RD}(x) & x \ge 0 \\ \operatorname{RU}(x) & x < 0 \end{cases}, & \operatorname{RA}(x) &= \begin{cases} \operatorname{RU}(x) & x \ge 0 \\ \operatorname{RD}(x) & x < 0 \end{cases}, \end{aligned}

而就近舍入在 RD⁡(x)\operatorname{RD}(x) 与 RU⁡(x)\operatorname{RU}(x) 中选取更接近 xx 的那个;出现平局(xx 恰在正中)时,要么取末位为偶数的候选(RN⁡even\operatorname{RN}_{\text{even}}),要么取绝对值较大的候选(RN⁡away\operatorname{RN}_{\text{away}})。11 IEEE 754-2019 第 4.3 条定义了 roundTiesToEven、roundTiesToAway、roundTowardPositive、roundTowardNegative 与 roundTowardZero。远离零舍入以及十进制的 “05up” 模式来自 General Decimal Arithmetic 规范(Cowlishaw)。 每个舍入函数都是单调的(x≤y⇒rnd⁡(x)≤rnd⁡(y)x \le y \Rightarrow \operatorname{rnd}(x) \le \operatorname{rnd}(y)),且保持 F\mathbb{F} 不变(对 y∈Fy \in \mathbb{F} 有 rnd⁡(y)=y\operatorname{rnd}(y) = y);区间算术与经认证的初等函数所依赖的正是这两条性质。

各个领域用各自的枚举来命名这些函数:

函数BinaryRoundingMode@lf_arith.RoundingModeDecimalRoundingMode / GdaRoundingMode
RN⁡even\operatorname{RN}_{\text{even}}RoundTiesToEvenToNearestEvenHalfEven
RN⁡away\operatorname{RN}_{\text{away}}RoundTiesToAway—HalfUp
就近舍入,平局时向零——HalfDown
RZ⁡\operatorname{RZ}RoundTowardZeroTowardZeroDown
RU⁡\operatorname{RU}RoundTowardPositiveTowardPositiveCeiling
RD⁡\operatorname{RD}RoundTowardNegativeTowardNegativeFloor
RA⁡\operatorname{RA}RoundAwayFromZeroAwayFromZeroUp
向零舍入,若保留的末位为 0 或 5 则改为远离零——ZeroFiveUp

with_precision(p, mode) 以新精度对值应用其中一个舍入函数。二进制的 0.10.1 是 0.0001100110011…20.0001100110011\ldots_2;取五位时截断形式为 110012⋅2−811001_2 \cdot 2^{-8},下一位是 1 且其后还有非零位,因此就近舍入向上进位:

///|
test "rounding 0.1 to five bits" {
  let tenth = @bin_float.BinFloat::from_double(0.1)
  inspect(tenth.with_precision(5, @lf_arith.ToNearestEven), content="13p-7")
  inspect(tenth.with_precision(5, @lf_arith.TowardZero), content="25p-8")
}

舍入到整数

舍入到整数值是同一构造,只是取 F=Z\mathbb{F} = \mathbb{Z}。BinFloat 提供固定方向的形式 floor(RD⁡\operatorname{RD})、ceil(RU⁡\operatorname{RU})、trunc(RZ⁡\operatorname{RZ})、round(就近,平局远离零)和 round_ties_even(就近,平局取偶);to_integral_value_ctx 使用上下文的方向且不引发标志,to_integral_exact_ctx 则在值发生变化时额外引发 inexact。整数转换 to_int_ctx、to_int64_ctx、to_uint_ctx 与 to_uint64_ctx 以相同方式舍入,对 NaN、无穷和超出范围的结果返回 None 并引发 invalid。结果保留零的符号:⌈−0.3⌉=−0\lceil -0.3 \rceil = -0。

///|
test "rounding to integers" {
  let x = @bin_float.BinFloat::from_double(2.5)
  inspect(x.round(), content="3p0")
  inspect(x.round_ties_even(), content="1p1")
  inspect(x.floor(), content="1p1")
  inspect(@bin_float.BinFloat::from_double(-0.3).ceil(), content="-0")
  let (n, flags) = x.to_int_ctx(
    @bin_float.BinaryContext::binary64(),
    exact=true,
  )
  inspect(n == Some(2), content="true")
  inspect(flags.inexact(), content="true")
}

ulp 与单位舍入误差

对基数 β\beta、精度 pp 以及满足 βe≤∣x∣<βe+1\beta^{e} \le |x| < \beta^{e+1} 的非零 xx,末位单位(unit in the last place)是 xx 附近 pp 位值之间的间距:

ulp⁡(x)=β e−p+1.\operatorname{ulp}(x) = \beta^{\,e - p + 1}.

BinFloat::ulp 在 β=2\beta = 2 下、以值自身的精度和无界指数范围计算它;对零返回 21−p2^{1-p},即 11 的 ulp。在有界上下文中,间距在次正规数阈值处(见下一节)不再缩小,因此那里极小值的 ulp 为 β emin⁡−p+1\beta^{\,e_{\min} - p + 1}。

单位舍入误差 uu 界定一次舍入的相对误差。取范围内满足 βe≤∣x∣<βe+1\beta^{e} \le |x| < \beta^{e+1} 的 xx。两个相邻值 RD⁡(x)\operatorname{RD}(x) 与 RU⁡(x)\operatorname{RU}(x) 位于同一个 binade 中(或其中之一为 ±βe+1\pm\beta^{e+1}),相距一个 ulp,因此

∣RN⁡(x)−x∣≤12ulp⁡(x)=12β e−p+1=12β1−p⋅βe≤12β1−p ∣x∣,∣RD⁡(x)−x∣, ∣RU⁡(x)−x∣<ulp⁡(x)≤β1−p ∣x∣.\begin{aligned} |\operatorname{RN}(x) - x| &\le \tfrac{1}{2}\operatorname{ulp}(x) = \tfrac{1}{2}\beta^{\,e-p+1} = \tfrac{1}{2}\beta^{1-p} \cdot \beta^{e} \le \tfrac{1}{2}\beta^{1-p}\,|x|, \\ |\operatorname{RD}(x) - x|,\ |\operatorname{RU}(x) - x| &< \operatorname{ulp}(x) \le \beta^{1-p}\,|x|. \end{aligned}

由此得到浮点算术的标准模型:22 N. J. Higham,Accuracy and Stability of Numerical Algorithms,第 2 版,SIAM 2002,§2.2。含 η\eta 的下溢形式即该书定理 2.3;Goldberg 的 “What every computer scientist should know about floating-point arithmetic”(ACM Computing Surveys 23(1),1991)对二进制情形给出了相同的推导。

fl⁡(x∘y)=(x∘y)(1+δ),∣δ∣≤u={12β1−pnearest rounding,β1−pdirected rounding,\operatorname{fl}(x \circ y) = (x \circ y)(1 + \delta), \qquad |\delta| \le u = \begin{cases} \tfrac{1}{2}\beta^{1-p} & \text{nearest rounding}, \\ \beta^{1-p} & \text{directed rounding}, \end{cases}

只要精确值既不在上溢范围内、也不低于正规范围,该模型就成立。低于正规范围时误差改为绝对误差:存在次正规数时,fl⁡(x∘y)=(x∘y)(1+δ)+η\operatorname{fl}(x \circ y) = (x \circ y)(1+\delta) + \eta,其中 δη=0\delta\eta = 0,且就近舍入下 ∣η∣≤12β emin⁡−p+1|\eta| \le \tfrac{1}{2}\beta^{\,e_{\min}-p+1}。对 binary64(p=53p = 53)有 u=2−53u = 2^{-53};对 decimal64(p=16p = 16)有 u=12⋅10−15u = \tfrac{1}{2}\cdot 10^{-15}。

///|
test "ulp of one tenth" {
  let ctx = @bin_float.BinaryContext::binary64()
  let (tenth, _) = @bin_float.BinFloat::from_string_ctx("0.1", ctx).unwrap()
  inspect(tenth, content="3602879701896397p-55")
  // 0.1 lies in [2^-4, 2^-3), so ulp = 2^(-4 - 53 + 1).
  inspect(tenth.ulp(), content="1p-56")
  let (third, flags) = @bin_float.BinFloat::from_int(1).div_ctx(
    @bin_float.BinFloat::from_int(3),
    ctx,
  )
  inspect(third.to_shortest_string(), content="0.3333333333333333")
  inspect(flags.inexact(), content="true")
  let (digits, _) = third.to_decimal_string_ctx(20, ctx)
  inspect(digits, content="3.3333333333333331483e-1")
}

上下文:精度、指数范围与微小性(tininess)

上下文确定 F\mathbb{F} 和舍入函数。没有任何包读取环境中的舍入模式;上下文只是一个普通的不可变参数。

  • BinaryContext 包含精度 pp(比特数)、一个 BinaryRoundingMode、针对首位的可选指数界 emin⁡,emax⁡e_{\min}, e_{\max},以及一个 TininessDetection。正规值满足 2emin⁡≤∣x∣≤(2−21−p) 2emax⁡2^{e_{\min}} \le |x| \le (2 - 2^{1-p})\,2^{e_{\max}};低于 2emin⁡2^{e_{\min}} 时,次正规数保持固定间距 2 emin⁡−p+12^{\,e_{\min}-p+1}(渐进下溢)。binary16、binary32、binary64 与 binary128 是 IEEE 交换格式的预设。unbounded(p) 以及缺省的界使用实现范围 [binary_implementation_e_min,binary_implementation_e_max]=[1−230, 230−1][\texttt{binary\_implementation\_e\_min}, \texttt{binary\_implementation\_e\_max}] = [1 - 2^{30},\ 2^{30} - 1],且所有精度都以 binary_precision_max=228\texttt{binary\_precision\_max} = 2^{28} 比特为上限。超出实现范围的结果会被归类处理(按舍入方向上溢为无穷或最大有限值、下溢为零或最小次正规数),而绝不会以饱和的指数存储。
  • 普通二进制运算符(add、sub、mul、div、+、-、*、/)在该无界上下文中以两个操作数中较大的精度进行就近舍入(偶数优先),并丢弃标志。
  • DecimalContext(IEEE)与 GdaContext(GDA)包含以数位计的精度、舍入模式、针对调整指数 q+(digits of c)−1q + (\text{digits of } c) - 1 的 emin⁡e_{\min} 与 emax⁡e_{\max},以及一个 clamp 开关。次正规数可使用的最小指数为 Etiny=emin⁡−p+1E_{\text{tiny}} = e_{\min} - p + 1。设置 clamp 后,指数超过 emax⁡−p+1e_{\max} - p + 1 的结果会以零填充并引发 clamped,这正是交换格式的要求。例如 decimal64 的参数为 p=16p = 16、emax⁡=384e_{\max} = 384、emin⁡=−383e_{\min} = -383。
  • **微小性(tininess)**决定一个极小结果何时算作下溢:舍入前检测比较精确值与 βemin⁡\beta^{e_{\min}},舍入后检测比较假定指数范围无界时的舍入值。IEEE 754 对二进制格式允许任一种方式;GDA 总是在舍入前检测。

标志、错误与包络

floating 通过四个互不替代的通道报告问题:

通道载体是否返回值?含义
状态标志与结果并列的 BinaryFlags、DecimalFlags、BallFlags是,即 IEEE 所定义的值在产生已定义结果的过程中出现了某种情况
GDA 状态与陷阱GdaOutcome, GdaContext::status是,触发陷阱时也返回粘滞的条件;已启用的陷阱会把结果标记为 Trapped
checked 错误Result[_, ArithmeticError]、*Result 包装类型否在 checked 契约下无法产生所请求的标量
包络BallFloat, BallFloatDecorated是,一个集合所有可能的精确结果都落在返回的区间内

IEEE 标志

五种 IEEE 异常33 IEEE 754-2019 第 7 条。默认异常处理返回此处列出的值并引发状态标志;floating 恰好实现这一默认行为,从不改变控制流。 在 BinaryFlags 中以布尔值报告,在 DecimalFlags 中以 DecimalSignal 成员报告:

  • invalid operation(无效运算):不存在有意义的实数结果(∞−∞\infty - \infty、0⋅∞0 \cdot \infty、−1\sqrt{-1}、对信号 NaN 的任何运算);结果为静默 NaN。
  • division by zero(除以零):由有限操作数得到精确的无穷结果(1/−0=−∞1/{-0} = -\infty、log⁡0=−∞\log 0 = -\infty)。
  • overflow(上溢):在无界指数下的舍入结果将超过最大有限值;按舍入方向,结果为 ±∞\pm\infty 或最大有限值,并同时引发 inexact。
  • underflow(下溢):结果是微小的(按上下文的微小性规则)且不精确。
  • inexact(不精确):舍入结果与精确值不同。

Decimal 另外增加了 GDA 条件 rounded(丢弃了数位,即使丢弃的是零)、clamped、subnormal、conversion syntax、division impossible、division undefined、invalid context 与 lost digits。标志从不取代值。若要在多个步骤中累积标志,可自行 combine,或使用 decimal_checked,它同时保留最近一次的(raised)与累积的(flags)标志集合。

///|
test "flags report conditions beside a defined value" {
  let ctx = @bin_float.BinaryContext::binary64()
  let one = @bin_float.BinFloat::from_int(1)
  let (pole, pole_flags) = one.div_ctx(
    @bin_float.BinFloat::negative_zero(),
    ctx,
  )
  inspect(pole, content="-inf")
  inspect(pole_flags.division_by_zero(), content="true")
  let (huge, huge_flags) = @bin_float.BinFloat::from_double(1.0e308).mul_ctx(
    @bin_float.BinFloat::from_int(10),
    ctx,
  )
  inspect(huge, content="inf")
  inspect(huge_flags.overflow() && huge_flags.inexact(), content="true")
  let inf = @bin_float.BinFloat::inf(@def.Positive)
  let (undefined, invalid) = inf.sub_ctx(inf, ctx)
  inspect(@def.is_nan(undefined), content="true")
  inspect(invalid.invalid_operation(), content="true")
}

GDA 状态与陷阱

decimal_gda 遵循 General Decimal Arithmetic 模型。每个运算返回一个 GdaOutcome,其中包含已定义的结果、下一个上下文以及本次运算引发的标志。下一个上下文的 status() 是迄今为止所有引发条件的粘滞并集。当某个引发的条件在上下文的陷阱集合中已启用时,结果为 Trapped(signal, value, context, raised) 而非 Completed(value, context, raised);已定义的结果依然保留。若同时引发多个已启用的条件,被捕获的信号按以下优先级取第一个:invalid operation、division by zero、division undefined、division impossible、invalid context、conversion syntax、overflow、underflow、subnormal、inexact、rounded、clamped、lost digits。

///|
test "a GDA trap keeps the defined result" {
  let ctx = @decimal_gda.GdaContext::decimal64().trap(
    @decimal_gda.DivisionByZero,
  )
  let one = @decimal_gda.Decimal::from_string("1").unwrap()
  let zero = @decimal_gda.Decimal::from_string("0").unwrap()
  match @decimal_gda.divide(one, zero, ctx) {
    Trapped(signal, value, next, _) => {
      inspect(signal == @decimal_gda.DivisionByZero, content="true")
      inspect(value, content="inf")
      inspect(next.status().division_by_zero, content="true")
    }
    Completed(_, _, _) => fail("an enabled trap must fire")
  }
}

checked 错误

ArithmeticError(由 def 重新导出)具有一个种类:DivisionByZero、ParseError、DomainError、FormatError、UnsupportedOperation、UnorderedComparison 或 CertificationFailure。它用于 checked 契约无值可返回的情形:div_checked 除以零、对负数求 sqrt、compare_checked 遇到 NaN、字面量格式错误。BinFloatResult 与 BallFloatResult 保留第一个错误并跳过其余步骤。

初等函数两种形式都有。若认证细化无法在预算内确定舍入,try_*_ctx 函数返回带有 CertificationFailure 详情的 Err。非 try 形式从不中止:二进制版本返回静默 NaN 并引发 invalid operation,十进制与 GDA 版本返回各自的无效结果,以便标志与陷阱照常生效。

包络

BallFloat 表示一个以二进制数为端点的闭实数集 X=[x‾,x‾]X = [\underline{x}, \overline{x}],或特殊集合 Empty(∅\varnothing)与 Entire(R\mathbb{R})之一。函数 ff 的区间扩展 FF 必须满足包含性质

{ f(x):x∈X }⊆F(X),\{\, f(x) : x \in X \,\} \subseteq F(X),

向外舍入保证了这一点:精确下端点用 RD⁡\operatorname{RD} 舍入,精确上端点用 RU⁡\operatorname{RU} 舍入,由 RD⁡(a)≤a\operatorname{RD}(a) \le a 与 b≤RU⁡(b)b \le \operatorname{RU}(b) 可知存储的区间包含精确区间。44 R. E. Moore、R. B. Kearfott 与 M. J. Cloud,Introduction to Interval Analysis,SIAM 2009,第 3 章;集合与装饰模型见 IEEE 1788-2015 第 10–11 条。 宽的包络也是正确结果;紧致性是质量指标,而非契约。除以包含零的区间可能返回 Entire。

BallFloatDecorated 增加了一个 IEEE 1788 装饰(decoration),描述 ff 在 XX 上的已知性质:Com(在有界 XX 上有定义、连续且有界)、Dac(有定义且连续)、Def(有定义)、Trv(一无所知)以及 Ill(该区间为 NaI,即 “not an interval”)。NaI 不同于 Empty。装饰并非标量标志;BallFlags 报告的是 BallContext 的精度与范围事件。

///|
test "an enclosure contains every exact result" {
  let one = @ball_float.BallFloat::from_int(1, precision=53)
  let three = @ball_float.BallFloat::from_int(3, precision=53)
  let third = one.div(three)
  inspect(third.contains(@bin_float.BinFloat::from_double(1.0 / 3.0)), content="true")
  let around_zero = @ball_float.BallFloat::from_bounds(
    @bin_float.BinFloat::from_int(-1),
    @bin_float.BinFloat::from_int(1),
  )
  inspect(one.div(around_zero).is_entire(), content="true")
  let partly_defined = @ball_float.BallFloatDecorated::new(
    @ball_float.BallFloat::from_bounds(
      @bin_float.BinFloat::from_int(-1),
      @bin_float.BinFloat::from_int(4),
    ),
  )
  inspect(partly_defined.decoration(), content="com")
  inspect(partly_defined.sqrt_interval().decoration(), content="trv")
}

量子与同值类

对十进制值而言,运算返回同值类中的哪个成员是其契约的一部分。当精确结果在精度范围内可以容纳时,IEEE 754 与 GDA 选择具有理想指数的成员:55 IEEE 754-2019 第 5.2 条及表 5.1;Cowlishaw,General Decimal Arithmetic Specification 1.70,“Arithmetic operations”。

q(x+y)=q(x−y)=min⁡(qx,qy),q(x⋅y)=qx+qy,q(x/y)=qx−qy(when the quotient is exact),\begin{aligned} q(x + y) = q(x - y) &= \min(q_x, q_y), \\ q(x \cdot y) &= q_x + q_y, \\ q(x / y) &= q_x - q_y \quad (\text{when the quotient is exact}), \end{aligned}

而当结果必须舍入时,选择能容纳的系数最大的成员。解析会保留字面量的量子。quantize(x, y) 将 xx 舍入到 yy 的量子;reduce_ctx(GDA reduce)与 normalized() 去除末尾的零。数值比较只看值;same_quantum 以及全序 compare_total(IEEE)与 compare_total(GDA)还会考虑同值类。

///|
test "decimal results keep the ideal exponent" {
  let ctx = @decimal.DecimalContext::decimal64()
  let a = @decimal.Decimal::from_string("1.20").unwrap()
  let b = @decimal.Decimal::from_string("1.3").unwrap()
  inspect(a.add_ctx(b, ctx).0, content="2.50")
  inspect(a.mul_ctx(b, ctx).0, content="1.560")
  let c = @decimal.Decimal::from_string("2.400").unwrap()
  let d = @decimal.Decimal::from_string("1.2").unwrap()
  inspect(c.div_ctx(d, ctx).0, content="2.00")
  let (cents, flags) = @decimal.Decimal::from_string("2.345")
    .unwrap()
    .quantize(@decimal.Decimal::from_string("0.01").unwrap(), ctx)
  inspect(cents, content="2.34")
  inspect(flags.contains(@decimal.Inexact), content="true")
  let x = @decimal.Decimal::from_string("12.30").unwrap()
  let y = @decimal.Decimal::from_string("12.3").unwrap()
  inspect(x.compare(y), content="0")
  inspect(x.compare_total(y), content="-1")
}

带符号零与 NaN

零

零带有符号,从而 1/+0=+∞1/{+0} = +\infty 与 1/−0=−∞1/{-0} = -\infty 能够保留下溢的方向。规则遵循 IEEE 754-2019 第 6.3 条:

  • 积或商的符号为操作数符号的异或;
  • 符号相反的操作数之和若精确为零,或 x−xx - x,其结果均为 +0+0——除非舍入方向为 RD⁡\operatorname{RD},此时结果为 −0-0;
  • −0=−0\sqrt{-0} = -0,且舍入到整数时保留符号(⌈−0.3⌉=−0\lceil -0.3 \rceil = -0);
  • 数值比较将 −0-0 与 +0+0 视为相等。

NaN

静默 NaN 在算术中悄无声息地传播;信号 NaN 在被运算使用时引发 invalid operation,并在结果中变为静默 NaN。NaN 带有符号和一个整数载荷(nan_payload);含 NaN 操作数的运算返回由其中第一个 NaN 派生的静默 NaN。

///|
test "zero and NaN rules" {
  let ctx = @bin_float.BinaryContext::binary64()
  let down = @bin_float.BinaryContext::binary64(rounding=RoundTowardNegative)
  let one = @bin_float.BinFloat::from_int(1)
  inspect(one.sub_ctx(one, ctx).0.is_negative_zero(), content="false")
  inspect(one.sub_ctx(one, down).0.is_negative_zero(), content="true")
  inspect(@bin_float.BinFloat::negative_zero().sqrt_ctx(ctx).0, content="-0")
  let (quieted, flags) = @bin_float.BinFloat::signaling_nan().add_ctx(one, ctx)
  inspect(quieted.is_quiet_nan(), content="true")
  inspect(flags.invalid_operation(), content="true")
}

比较

IEEE 比较有四种结果:小于、等于、大于以及无序,后者在任一操作数为 NaN 时出现。不同的 API 提供不同的序,选对序非常重要:

API序NaN−0-0 与 +0+0
compare_quiet, less_quiet, equal_quiet, … (bin_float)IEEE 偏序,@def.PartialOrderUnordered;仅对信号 NaN 引发 invalid相等
compare_signaling, less_signaling, … (bin_float)IEEE 偏序Unordered;对任何 NaN 都引发 invalid相等
compare_checked数值序带有 UnorderedComparison 的 Err相等
compare、<、<=、排序(Compare)全预序所有 NaN 彼此相等,且大于所有数相等
total_order、total_order_compare、total_order_mag(bin_float)、compare_total(decimal)作用于表示的 IEEE totalOrder按符号、种类和载荷排序−0<+0-0 < +0

compare 将 NaN 排在所有数之上,使 Compare 成为全预序、排序永不失败;它从不中止。当必须把 NaN 视为无序时,请使用静默谓词或 compare_checked。

相等性取决于类型。Decimal 的 == 对有限值按数值比较(-0 == 0.00),并将两个 NaN 视为相等。BinFloat 与 BallFloat 派生了 Eq,因此它们的 == 比较的是表示,包括零的符号和精度:在这里 -0 == +0 为假,而 compare 返回 0。比较二进制值的数值相等性请使用 compare(...) == 0 或 equal_quiet。

///|
test "choosing a comparison" {
  let nan = @bin_float.BinFloat::nan()
  let one = @bin_float.BinFloat::from_int(1)
  inspect(nan.compare(one), content="1")
  inspect(nan > @bin_float.BinFloat::inf(@def.Positive), content="true")
  inspect(nan.compare_checked(one) is Err(_), content="true")
  let (order, flags) = nan.compare_quiet(one)
  inspect(order == @def.Unordered, content="true")
  inspect(flags.invalid_operation(), content="false")
  let neg_zero = @bin_float.BinFloat::negative_zero()
  let pos_zero = @bin_float.BinFloat::zero()
  inspect(neg_zero.compare(pos_zero), content="0")
  inspect(neg_zero.total_order_compare(pos_zero), content="-1")
  inspect(neg_zero == pos_zero, content="false")
}

基数之间的转换

每个二进制小数都有有限的十进制展开,因为 2−k=5k⋅10−k2^{-k} = 5^{k} \cdot 10^{-k};而大多数十进制小数没有有限的二进制展开,因为 10−k=2−k5−k10^{-k} = 2^{-k}5^{-k},而 5−k5^{-k} 不是二进有理数。因此十进制到二进制的转换通常不精确,二进制到十进制的转换则是精确的,但可能需要很多数位。BinFloat::from_string_ctx(IEEE convertFromDecimalCharacter)与 to_decimal_string_ctx(convertToDecimalCharacter)对任意精度和指数都是正确舍入的;to_shortest_string 打印能读回同一值的最少数位,0.1 + 0.2 正是借此显示出其舍入误差。

交换格式转换的范围更窄:BinaryInterchange(binary16/32/64/128)以及 decimal32/64/128 的 DPD 与 BID 编码固定了字段宽度、指数界和特殊值编码。semantic 将各个包的值投影为精确有理数,因此能分辨十进制 0.10.1 与二进制 fl⁡(0.1)\operatorname{fl}(0.1) 是不同的数;该投影有意丢弃精度、量子、带符号零、载荷、装饰和标志。

///|
test "decimal 0.1 is not binary 0.1" {
  let sum = @bin_float.BinFloat::from_double(0.1).add(
    @bin_float.BinFloat::from_double(0.2),
  )
  inspect(sum.to_shortest_string(), content="0.30000000000000004")
  let decimal_tenth = @semantic.SemanticScalar::from_decimal(
    @decimal.Decimal::from_string("0.1").unwrap(),
  )
  let binary_tenth = @semantic.SemanticScalar::from_bin_float(
    @bin_float.BinFloat::from_double(0.1),
  )
  inspect(decimal_tenth == binary_tenth, content="false")
  let decimal_half = @semantic.SemanticScalar::from_decimal(
    @decimal.Decimal::from_string("0.500").unwrap(),
  )
  let binary_half = @semantic.SemanticScalar::from_bin_float(
    @bin_float.BinFloat::from_double(0.5),
  )
  inspect(decimal_half == binary_half, content="true")
}

决策清单

选择 API 前应回答:

  1. 结果是标量值、表示,还是实数集合?
  2. 基数、精度、指数范围或量子是否必须保持可观察?
  3. 调用方需要逐运算的标志、GDA 的粘滞状态与陷阱,还是可短路的 checked 错误?
  4. 是否可能出现 NaN、无穷、带符号零、Empty、Entire 或 NaI?
  5. 数值序是否足够,还是需要 IEEE 偏序、全序或集合关系?
  6. 转换是任意精度的,还是固定的交换格式?
  7. 哪些固定版本的符合性证据支持该结论?参见验证。

Footnotes

  1. IEEE 754-2019 第 4.3 条定义了 roundTiesToEven、roundTiesToAway、roundTowardPositive、roundTowardNegative 与 roundTowardZero。远离零舍入以及十进制的 “05up” 模式来自 General Decimal Arithmetic 规范(Cowlishaw)。 ↩

  2. N. J. Higham,Accuracy and Stability of Numerical Algorithms,第 2 版,SIAM 2002,§2.2。含 η\eta 的下溢形式即该书定理 2.3;Goldberg 的 “What every computer scientist should know about floating-point arithmetic”(ACM Computing Surveys 23(1),1991)对二进制情形给出了相同的推导。 ↩

  3. IEEE 754-2019 第 7 条。默认异常处理返回此处列出的值并引发状态标志;floating 恰好实现这一默认行为,从不改变控制流。 ↩

  4. R. E. Moore、R. B. Kearfott 与 M. J. Cloud,Introduction to Interval Analysis,SIAM 2009,第 3 章;集合与装饰模型见 IEEE 1788-2015 第 10–11 条。 ↩

  5. IEEE 754-2019 第 5.2 条及表 5.1;Cowlishaw,General Decimal Arithmetic Specification 1.70,“Arithmetic operations”。 ↩