ryu4cj:

分支2Tags0
文件最后提交记录最后更新时间
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前

ryu4cj

Ryu 浮点数 ↔ 十进制字符串转换库的 Cangjie 移植版。

Ryu 是目前已知最快的浮点数最短表示转换算法,由 Ulf Adams 提出,发表于 PLDI'18 与 OOPSLA'19。 本移植将 Ryu 的 C 实现全部功能以 Cangjie 语言重新实现,对外接口命名与 C 头文件逐字一致, 内部实现保持算法等价。项目以 cjpm 静态库形式发布,可被任意 Cangjie 工程依赖。

简介

Ryu 解决的核心问题是:为 IEEE 754 浮点数生成能保证往返安全(round-trip safe)的最短十进制表示。 即,对生成的字符串做正确解析,能恢复出精确的原始浮点值。例如 32 位浮点数 0.300000011920928955078125 的最短往返安全表示就是 0.3

传统算法(如 printf("%.17g")strtod)通常依赖大整数除法或迭代搜索,既缓慢又难以保证最短性。 Ryu 在固定宽度(64/128 位)整数运算内直接求出结果,无需任意精度算术,因此显著快于常见实现。

本移植覆盖 Ryu 的全部转换模式:

IEEE 类型 支持的输出格式
32 位 (Float32) 最短
64 位 (Float64) 最短、定点 (%f)、指数 (%e)
16/80/128 位 最短(经 generic_128 通用路径)

参考文献:

API

所有接口位于包 ryu,命名与对应 C 头文件完全一致。

缓冲型接口说明*_buffered_n 函数将结果以 UTF-8 字节写入 Array<UInt8> 并返回写入长度(不追加终止符)。 若需得到 String,用 String.fromUtf8(result[0..n]) 转换;也可直接使用对应的便捷函数(如 d2s), 它内部已封装此转换。

浮点数 → 最短字符串(ryu.h

// 写入 result,返回写入字节数(不追加终止符)。调用方需预分配足够容量。
func d2s_buffered_n(f: Float64, result: Array<UInt8>): Int64   // 最大 25 字节
func f2s_buffered_n(f: Float32, result: Array<UInt8>): Int64   // 最大 15 字节

// 便捷封装:内部分配缓冲区,返回格式化后的 String。
func d2s(f: Float64): String
func f2s(f: Float32): String

浮点数 → 定点 / 指数格式(ryu.h

// %.Nf 风格定点格式。
func d2fixed_buffered_n(d: Float64, precision: UInt32, result: Array<UInt8>): Int64
func d2fixed(d: Float64, precision: UInt32): String

// %.Ne 风格指数格式。
func d2exp_buffered_n(d: Float64, precision: UInt32, result: Array<UInt8>): Int64
func d2exp(d: Float64, precision: UInt32): String

字符串 → 浮点数(ryu_parse.h

public enum Status {
    | SUCCESS
    | INPUT_TOO_SHORT
    | INPUT_TOO_LONG
    | MALFORMED_INPUT
}

func s2d_n(buffer: String, len: Int64): (Status, Float64)
func s2d(buffer: String): (Status, Float64)

func s2f_n(buffer: String, len: Int64): (Status, Float32)
func s2f(buffer: String): (Status, Float32)

通用 128 位转换(ryu_generic_128.h

public let FD128_EXCEPTIONAL_EXPONENT: Int32 = 0x7FFFFFFF

// 十进制浮点中间表示 (-1)^sign * mantissa * 10^exponent。
// exponent == FD128_EXCEPTIONAL_EXPONENT 时表示 NaN / ±Infinity。
public struct floating_decimal_128 {
    public let mantissa: UInt128
    public let exponent: Int32
    public let sign: Bool
    public init(mantissa: UInt128, exponent: Int32, sign: Bool)
}

// 将任意 IEEE 位模式(最多 128 位)转为最短十进制浮点表示。
func generic_binary_to_decimal(
    bits: UInt128, mantissaBits: UInt32, exponentBits: UInt32,
    explicitLeadingBit: Bool): floating_decimal_128

func float_to_fd128(f: Float32): floating_decimal_128
func double_to_fd128(d: Float64): floating_decimal_128

func generic_to_chars(v: floating_decimal_128, result: Array<UInt8>): Int64
func generic_to_chars(v: floating_decimal_128): String

位重解释辅助(common.h

func float_to_bits(f: Float32): UInt32
func double_to_bits(d: Float64): UInt64
func bits_to_float32(bits: UInt32): Float32
func bits_to_float64(bits: UInt64): Float64

C 原版仅以局部 memcpy 实现位重解释;bits_to_float32 / bits_to_float64 为 Cangjie 新增的逆向辅助。

用法示例

import ryu.*

let s1 = d2s(1.0 / 3.0)          // "0.3333333333333333"
let s2 = d2fixed(3.14159, 2)     // "3.14"
let s3 = d2exp(12345.678, 1)     // "1.2e4"

let (status, value) = s2d("1.5") // (Status.SUCCESS, 1.5)

构建与测试

环境说明

  • Cangjie 工具链(cjnative)
  • 构建工具 cjpm

本项目已通过以下两个版本的构建与测试验证:

版本 渠道
1.0.5 LTS(长期支持版)
1.1.3 STS(短期支持版)

注意:不同版本工具链生成的编译中间文件二进制不兼容。切换工具链版本后,请先运行 cjpm clean 清理之前工具链的中间产物,再重新构建。

构建命令

cjpm build          # 构建静态库

cjpm.tomlcompile-option = "--int-overflow=wrapping" 是正确性的硬性要求: Ryu 算法依赖无符号整数回绕语义,Cangjie 默认的溢出检查会触发运行时异常,必须关闭。

单元测试

cjpm test           # 运行全部 119 个单元测试
cjpm test --coverage  # 生成覆盖率报告(项目覆盖率约 98.7%)

测试位于 src/test/(子包 ryu.test),共 12 个文件,覆盖:

  • 各转换函数的边界值(0、次正规数、最大值、NaN、±Infinity)
  • 与上游 C 测试套件等价的逐例验证
  • 内部算法(mulShift64pow5bitsUInt128 运算等)的正确性
  • 解析路径的畸形输入检测(空串、超长、多小数点等)

基准测试

cjpm bench          # 运行 d2s / f2s / d2fixed / d2exp 及 stdlib 对照组

基准位于 src/benchmark/(子包 ryu.benchmark),使用 Cangjie 原生基准框架 (@Bench 宏),与标准库 Float64.format() / Float64.parse() 做对照。 典型结果(ryu 相对标准库的加速比):

操作 ryu / stdlib
d2s ~9×
d2fixed(17) ~2.5×
d2exp(6) ~5×
f2s ~8×
s2d (解析) ~0.6×

解析路径(s2d/s2f)慢于标准库,主因是 Cangjie 无原生 128 位整数, UInt128 软件模拟带来额外开销;转换路径(d2s/f2s/d2fixed/d2exp)则显著快于标准库。

项目结构

ryu4cj/
├── cjpm.toml                  # 包定义(name=ryu, static, --int-overflow=wrapping)
├── DESIGN.md                  # 设计文档(算法、API 对照、移植差异详解)
├── README.md                  # 本文件
├── src/
│   ├── ryu.cj                 # 包入口
│   ├── bit_utils.cj           # 位重解释(common.h)
│   ├── common.cj              # 共享辅助
│   ├── digit_table.cj         # "00".."99" 查表
│   ├── d2s.cj                 # Float64 → 最短串
│   ├── d2s_intrinsics.cj      # 64 位乘移内建
│   ├── f2s.cj                 # Float32 → 最短串
│   ├── f2s_intrinsics.cj      # 32 位乘移内建
│   ├── d2fixed.cj             # 定点 / 指数格式
│   ├── parse.cj               # 字符串解析(s2d + s2f)
│   ├── generic128.cj          # 通用 128 位路径
│   ├── uint128.cj             # UInt128 软件模拟类型
│   ├── tables_d2s.cj          # Float64 的 5 的幂拆分表
│   ├── tables_d2fixed.cj      # 定点格式查表
│   ├── tables_generic.cj      # 128 位路径的 5 的幂表
│   ├── test/                  # 子包 ryu.test(12 个测试文件,119 用例)
│   └── benchmark/             # 子包 ryu.benchmark(ryu_bench_test + std_bench_test)
└── html/                      # 覆盖率报告 / 基准对比图

许可证

本移植遵循上游 Ryu 的双许可证,使用者可任选其一:

  • Apache License 2.0
  • Boost Software License 1.0

每个源文件头部均保留原始版权声明(Ulf Adams, 2018)与本移植声明。 许可证全文见 LICENSE-Apache2LICENSE-Boost

链接