ryu4cj 是 Rust ryu 库面向仓颉语言的移植版本,用于将浮点数快速格式化为最短的十进制字符串表示。
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 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 通用路径) |
参考文献:
- PLDI'18 论文(最短转换正确性证明):https://dl.acm.org/doi/10.1145/3296979.3192369
- OOPSLA'19 论文(定点/指数转换正确性证明):https://dl.acm.org/doi/10.1145/3366395.3360595
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.toml 中 compile-option = "--int-overflow=wrapping" 是正确性的硬性要求:
Ryu 算法依赖无符号整数回绕语义,Cangjie 默认的溢出检查会触发运行时异常,必须关闭。
单元测试
cjpm test # 运行全部 119 个单元测试
cjpm test --coverage # 生成覆盖率报告(项目覆盖率约 98.7%)
测试位于 src/test/(子包 ryu.test),共 12 个文件,覆盖:
- 各转换函数的边界值(0、次正规数、最大值、NaN、±Infinity)
- 与上游 C 测试套件等价的逐例验证
- 内部算法(
mulShift64、pow5bits、UInt128运算等)的正确性 - 解析路径的畸形输入检测(空串、超长、多小数点等)
基准测试
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-Apache2 与 LICENSE-Boost。
链接
- 上游 C 实现(Ulf Adams):https://github.com/ulfjack/ryu
- 本移植仓库:https://gitcode.com/xusiwei1236/ryu4cj
- Ryu 其他语言移植列表见上游 README