news 2026/9/13 1:39:53

深入解析 miniredis fpconv:Go 移植的 Redis 7.2 Grisu2 浮点数转字符串算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析 miniredis fpconv:Go 移植的 Redis 7.2 Grisu2 浮点数转字符串算法

深入解析 miniredis fpconv:Go 移植的 Redis 7.2 Grisu2 浮点数转字符串算法

【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki

导读

本文围绕 Loki 仓库中 vendor 的第三方依赖 miniredis(vendor/github.com/alicebob/miniredis/v2)所附带的 fpconv 包展开。fpconv 的核心定位非常明确:把 Redis 7.2 中负责浮点数转字符串(float → string)的 C 代码逐行翻译为 Go,从而在模拟 Redis 时产出与真实 Redis 逐字节一致的响应。通过本文,你将掌握 fpconv 的对外 API、Grisu2 算法的 Go 实现细节(定点表示、边界计算、缓存 10 的幂、逐位出数字、三种输出格式决策),以及该包在 Loki 测试体系(如 Redis 缓存 mock)中的实际作用。

fpconv 是什么:一份“照抄 Redis 逻辑”的浮点格式化移植

vendor/github.com/alicebob/miniredis/v2/fpconv/README.md全文只有两句话,却交代了最关键的三个事实:

  1. 它是Redis 7.2 中浮点数转字符串 C 代码的翻译(translation)
  2. Go 标准库的strconv其实“足够接近”(close enough);
  3. 但既然可以“使用一模一样的逻辑”(use the exact same logic),就选择完全对齐,而不是用近似实现。

这决定了 fpconv 的设计哲学:行为一致性优先于重新发明轮子。在 miniredis 的 RESP 响应写入路径中,这一点体现得很直接:

// vendor/github.com/alicebob/miniredis/v2/server/server.go#L515-L518 // formatFloat formats a float the way redis does. // Redis uses a method called "grisu2", which we ported from C. func formatFloat(v float64) string { return fpconv.Dtoa(v) }

注释明确写到:Redis 使用一种叫grisu2的方法,miniredis 从 C 移植了它。fpconv.Dtoa(v)就是整个包对外唯一的入口函数,所有内部算法(grisu2emit_digitsgenerate_digits等)都被封装在这一行调用之后。

为什么要“一模一样”而不是用 strconv?

README 中 “Strconv does a close enough job” 的表述暗示了一个工程判断:浮点数格式化存在舍入边界(如 0.1、2.675 这类十进制无法精确表示的数值),不同实现可能在“最近表示”的取舍上产生细微差异。对于 miniredis 这类测试用 Redis 模拟器而言,响应字节的任何不一致都可能导致断言失败。因此与其依赖strconv的“足够接近”,不如直接复用 Redis 自身的行为,让 mock 结果与真实 Redis 输出完全一致。

包结构:五个文件组成一个自洽的算法单元

fpconv 目录下共有 5 个文件(另有 LICENSE.txt):

文件职责
dtoa.go对外入口Dtoa、特殊值过滤、grisu2 主流程、数字输出与舍入
fp.go64 位定点数Fp结构及构造、归一化、乘法、边界计算
powers.go预计算的 10 的幂缓存表与查找函数
README.md包定位说明(本文主体)
Makefile构建辅助

对外入口:Dtoa 的三段式流程

dtoa.go 中的Dtoa(d float64) string整体流程清晰:

  1. 符号处理:通过get_dbits(d)&signmask判断符号位,为负数写入'-'
  2. 特殊值过滤:调用filter_special,处理 0、NaN、Inf 这些不需要 grisu2 参与的值;
  3. 常规数值:调用grisu2生成十进制数字序列与指数信息,再交给emit_digits排版成最终字符串。

特殊值:零、NaN、无穷大的输出

filter_special 的规则非常明确:

  • fp == 0.0→ 输出"0"
  • math.IsNaN(fp)→ 输出"nan"
  • math.IsInf(fp, 0)→ 输出"inf"(正负号由外层符号位负责)。

值得注意的是,由于符号位在特殊值判断之前写入,从源码结构可以推断:-0.0-NaN-Inf会被输出为"-0""-nan""-inf"。另外,Dtoa内部使用dest [25]rune作为输出缓冲区,源码注释特别指出 “Note C has 24, which is broken”,即原 C 版本的缓冲区大小存在隐患,Go 移植版将其修正为 25。

核心算法:Grisu2 的 Go 实现

Grisu2 是“以最短且可往返(round-trip)的十进制表示打印二进制浮点数”的经典算法。fpconv 的移植保留了原算法的全部阶段。

1. 定点表示与位域分解

fp.go 定义了核心类型:

type Fp struct { frac uint64 exp int64 }

Fp用“64 位无符号尾数 + 指数”表示浮点数的定点近似。build_fp 通过位运算分解 IEEE 754 double:

  • fracmask = 0x000FFFFFFFFFFFFF:取 52 位尾数;
  • expmask = 0x7FF0000000000000:取 11 位指数;
  • hiddenbit = 0x0010000000000000:规格化数的隐式最高位;
  • signmask = 0x8000000000000000:符号位;
  • expbias = 1023 + 52:指数偏置,同时抵消尾数位宽。

当指数域非 0 时补上 hiddenbit 并减去偏置;指数域为 0 则按次正规数(subnormal)处理,exp = -expbias + 1

2. 归一化与上下边界

normalize 将尾数左移直至 hiddenbit 位置位,再整体左移64-52-1位,把定点数扩展为 64 位表示。

get_normalized_boundaries 计算当前浮点数的可表示区间上下界(upper/lower),这是保证正确舍入的关键:算法只需要生成落在上下界之间的十进制数字,就能确保该表示是“最近且唯一”的。注意l_shift的处理:当fp.frac == hiddenbit(即恰好是 2 的整数幂)时,下界的构造方式不同(l_shift = 2),以处理边界上的间隔变化。

3. 缓存的 10 的幂

powers.go 预计算了一张 10 的幂表:

  • npowers = 87:共 87 个缓存项;
  • steppowers = 8:每 8 个指数间隔一个缓存项;
  • firstpower = -348:最小覆盖到10^-348
  • expmin = -60expmax = -32:查找时要求的“十进制指数 × 定点补偿”的合法区间。

每个缓存项是一个Fp(如{18054884314459144840, -1220}),即“10 的某次幂”的 64 位定点近似。find_cachedpow10 先用对数估算定位索引(one_log_ten = 0.30102999566398114,即 log10(2) 的倒数),再通过expmin/expmax区间微调,最终返回与目标指数最匹配的缓存项,并回传十进制指数k

4. 64 位定点乘法

multiply 实现两个Fp的定点乘法:把 64 位尾数拆成高 32 位与低 32 位,做四次 32×32 乘法(ah_blal_bhal_blah_bh),再按位置相加,并加上1 << 31做向上舍入(round up),最终指数为a.exp + b.exp + 64

5. 逐位生成十进制数字

grisu2 将原始浮点数、上下界统一乘上缓存幂后,交给 generate_digits 逐位产出数字:

  • 第一轮kappa = 10,从tens[10] = 1000000000开始,用除法逐位提取整数部分的高位数字;
  • 每提取一位就检查剩余部分是否已经落入 delta(上界与下界之差)之内,若满足则调用 round_digit 做就近舍入并结束;
  • 否则进入第二轮,part2每次乘 10 继续提取小数位,直到剩余部分小于 delta。

round_digit实现了“向最近偶数/最近值”的调整逻辑:当剩余误差rem小于fracdelta - rem足够大时,递减最后一位数字,使结果更接近真实值。

输出排版:emit_digits 的三种格式决策

emit_digits 根据十进制指数K与数字位数ndigits决定最终排版,与 Redis 原实现保持一致:

  1. 纯整数:当K >= 0 && exp < ndigits + 7时,直接输出全部数字并补K个零(例如1.5e3输出为1500);
  2. 普通十进制(不带科学计数法):当K < 0 && (K > -7 || exp < 4)时,fp < 1.0输出前导0.与补零(如0.0015),fp > 1.0则在对应位置插入小数点;
  3. 科学计数法:其余情况输出d.dddde±XX形式,指数部分支持到三位(exp > 99时先输出百位),并依据K + ndigits - 1的符号决定+/-。同时这里有一个细节:负数时科学计数法的数字上限从 18 减到 17(l := 18; if neg { l-- }),以容纳开头的负号。

正是这套与 C 版本逐行对应的决策逻辑,保证了 miniredis 输出的浮点字符串与真实 Redis 完全一致。

在 Loki 仓库中的实际用途:测试保真度的地基

fpconv 是 miniredis 的内部实现细节,而 miniredis 以 vendor 依赖的形式被 Loki 用于测试。从仓库源码可以确认其典型用法:

  • pkg/storage/chunk/cache/redis_cache_test.go 中miniredis.Run()启动内存版 Redis 服务器,用来为 Loki 的RedisCache存储/读取缓存测试提供 mock 后端;
  • pkg/storage/chunk/cache/redis_client_test.go 同样基于 miniredis 构造单机与集群两种 Redis 客户端测试场景;
  • pkg/logql/sketch/topk_test.go 引入了 miniredis 的hyperloglog能力用于 LogQL 草图(sketch)相关测试。

在这些测试中,只要被测试代码对 Redis 返回的浮点字符串做精确断言(例如 sorted set 的 score、HINCRBYFLOAT 类命令的结果),miniredis 的响应就必须和真实 Redis 逐字符一致。此时 fpconv 的意义就体现出来了:它让 mock 与真实行为之间不存在因格式化策略不同而产生的差异,从而保证测试结果可复现、可迁移到真实 Redis 环境。

小结

fpconv 是“用精确移植代替近似实现”的一个小而完整的范例:

  • 对外仅暴露Dtoa,内部完整复刻 Redis 7.2 的 Grisu2 浮点格式化算法;
  • 三个源文件各司其职:fp.go提供定点运算原语,powers.go提供预计算幂表,dtoa.go负责数字生成、舍入与三种格式的排版;
  • 在 Loki 仓库中,它作为 miniredis 测试依赖的一部分,为 Redis 缓存相关测试提供了与真实 Redis 完全一致的浮点字符串行为。

如果想继续深入,建议依次阅读 dtoa.go(算法主流程)、fp.go(定点运算原语)与 powers.go(幂表构造),再对照 server.go 中的调用点,即可完整串起“Redis 浮点格式化 → Go 移植 → 测试 mock”这条链路。

【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 1:37:46

TSMaster序列发送模块:汽车总线报文时序控制的自动化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 1:32:13

PostgreSQL JSON类型深度解析:json与jsonb选型、索引机制及生产实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 1:31:34

如何用 AI SDK 在 SvelteKit 项目中完成第一次流式聊天 Agent 开发

如何用 AI SDK 在 SvelteKit 项目中完成第一次流式聊天 Agent 开发 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents 项目地址: https://git…

作者头像 李华