log2 函数是我在代码评审里见得最多、也最容易被顺手写错的一个小东西。它表面上只是一行数学调用,实际上牵扯到二进制思维、浮点精度、整数位运算、容量规划四件完全不同的事。很多人第一次真正用上它,是在算“需要几位”“要扩容几次”“二分几次能找完”“这棵树有几层”这类问题上,然后写出一个ceil(log2(n))就交差,结果在 n 取某些特定值的时候悄悄偏了一格。这篇东西我想把它拆开讲:log2 函数到底在算什么、为什么工程里到处是它、整数域和浮点域为什么必须分家、主流语言各自的正确写法、以及我在真实项目里踩过的精度坑和性能坑。刚接触的读者可以把它当成一份带算例的入门手册,写过几年的人可以直接跳到第 5、6 节看实现和排查表。
1. 先把 log2 从课本里拽出来:它到底在算什么
1.1 用“对折几次”建立直觉
log2 函数回答的问题非常具体:一个数要连续除以 2 多少次,才会掉到 1 以下(或不大于 1)。你手里有 1024 张牌,每次对半分,分 10 次就分到单张,因为 2 的 10 次方是 1024,所以 log2(1024) = 10。这个“对折”的意象比任何公式都好用,因为它直接对应计算机里最有存在感的结构:二进制位。
我习惯用三个锚点把直觉钉死:log2(1) = 0,代表零次翻倍;log2(2) = 1,翻一次;log2(1024) = 10,翻十次。再补一个粗略换算 log2(10) ≈ 3.32,意思是“十进制每加一位,二进制大概多 3.32 位”。这条换算在估算存储、估算位宽、估算十进制转二进制的开销时特别顺手,比掏计算器快得多。
反过来说,2 的 n 次方就是 log2 的逆运算,这对关系决定了 log2 在工程里几乎总是和“2 的幂”绑在一起出现:数组扩容按 2 倍涨、哈希桶个数取 2 的幂、分治层数按 2 分、树的高度按 2 分叉。只要你的结构是二分的,log2 就一定会冒出来。
1.2 工程里 log2 的四类高频出场
我把实际遇到过的场景归了归类,基本逃不出下面四类。这张表可以当地图用,遇到问题先判断自己落在哪一格,再去对应的章节看细节。
| 场景类别 | 典型问题 | log2 承担的角色 | 常见误用 |
|---|---|---|---|
| 位宽与容量 | 装下 N 种状态要几位、桶数组开多大 | 向上取整的位数计算 | 用浮点 log2 算整数位数 |
| 算法复杂度 | 二分查找几次、归并几层、FFT 多少蝶形 | 层数/次数估算 | 把常数项当不存在 |
| 数据结构规划 | B+ 树几层、跳表几级、堆的高度 | 以分叉数为底的层数换算 | 忘了根节点和叶子层 |
| 度量与信息 | 熵多少比特、几个倍频程、几个 dB | 度量单位的换算 | 振幅和功率混用同一个系数 |
这里有个容易被忽略的点:大 O 记号里 log 的底数无所谓,但落到工程上底数就是常数因子,必须算清楚。log2(n) 和 log10(n) 差了 3.32 倍,如果你在一秒要跑几百万次的循环里算层数,这个 3.32 倍是实打实的开销;反过来如果你只是在启动时算一次容量,那用哪个底数都无所谓,可读性优先。判断标准很简单:这段代码在热路径上吗?在,就用整数位运算;不在,就用最直白的写法。
还有一类出场是“隐式”的,很多人用了却没意识到。比如你在配置文件里写max_connections = 65536,在日志里写“支持 6 万并发”,这个 65536 就是 2 的 16 次方,背后是 16 位的地址空间思维;再比如你把超时时间设成 1、2、4、8、16 秒的退避序列,那第 k 次重试的等待时间就是 2 的 k 次方,反过来“给定最大等待 60 秒,最多退避几次”就是一次 log2。这类隐式用法不出现在代码里,但它决定了你的参数设计合不合理。
2. log2 的数学底子:几个必须烂熟的点与边界
2.1 定义域、特殊值与单调性
log2 的定义域是正实数,x 必须大于 0。x 趋近 0 时结果趋近负无穷,x = 0 处无定义。这一点在代码里对应的就是“必须先判空、判零、判负”,任何直接log2(user_input)的写法都是定时炸弹,因为线上真的会有 0 和负数进来。
几个特殊值记牢:log2(1) = 0;log2(2) = 1;log2(0.5) = -1;log2(2 的 k 次方) = k 对任意整数 k 成立,包括负 k。函数在 (0, +∞) 上严格单调递增,这条性质在做“比较大小”“判断是否超过阈值”时很有用——如果 a < b,那 log2(a) < log2(b),不需要真去算指数。
一个实践中的细节:log2 在 1 附近非常“平”,在 0 附近非常“陡”。这意味着当你用它做“数量级判断”时,靠近 1 的输入对误差极其敏感。比如 log2(1.0000001) 约等于 1.44e-7,而浮点误差可能就有这个量级,这时候结果基本没意义。我在做指标分级(比如把 QPS 分成几档)时,会先把输入规整到“每档至少差 2 倍”这种量级,避免在 1 附近做精细判别。
2.2 换底公式在浮点下为什么会出错
教科书会告诉你 log2(x) = ln(x) / ln(2) = log10(x) / log10(2)。数学上没问题,工程上这是精度事故的第一大来源。原因在于两次舍入:先算 ln(x) 引入一次误差,再除以 ln(2)(本身也是个近似常数)引入第二次误差,误差还会被放大。
最典型的症状是本来该是整数的结果变成 28.999999999999996 或 29.000000000000004。如果你的代码后面跟了ceil或floor或int()截断,这一格偏差就变成了实打实的逻辑错误。我见过的最惨案例是分页计算:总条数除每页条数得到页数,中间用了 log 换底去算“需要几位来存页码”,结果某天数据量刚好卡在 2 的整次幂上,页码少了一页,最后一条数据永远查不出来。
所以第一条硬规矩:能用专门的 log2 接口就别自己换底。主流语言的数学库都提供了独立的 log2 实现,它不是简单地拿 ln 除一下,而是有针对性的算法(通常是把尾数规约到 [1, 2) 区间后用多项式逼近,指数部分直接由位模式取),在 2 的整数次幂上能做到精确返回整数。自己用换底公式拼,等于主动放弃了这份精度保障。
2.3 log2 与位运算的天然接口
这是我认为 log2 最迷人的地方:对整数来说,floor(log2(x)) 就是 x 的最高有效位的位置。x = 1 时是第 0 位,x = 8(二进制 1000)时是第 3 位,x = 1024 时是第 10 位。二者完全等价,而位运算版本快了一个数量级还不带浮点误差。
这个等价关系不是巧合,因为 log2 问的本来就是“2 的几次方能到这儿”,而二进制表示里每一位的权重就是 2 的幂。理解了这层,你再看31 - clz(x)、x.bit_length() - 1、Math.clz32、_BitScanReverse这些看起来古怪的接口,就都顺了:它们都是“求最高有效位位置”的不同叫法,也就是整数 log2 的不同马甲。
顺带说一个常被问的问题:为什么很多数据结构坚持把容量取成 2 的幂?因为一旦容量是 2 的 k 次方,取模运算就可以写成位与:hash % capacity变成hash & (capacity - 1),在多数 CPU 上快好几倍;同时“扩容”也变成纯粹的左移一位,扩容前后的桶映射关系很规整,rehash 时元素要么留在原位要么往后跳固定距离。而“把 n 向上取整到 2 的幂”这个操作,本身就是一次 ceil(log2(n)) 再加一次移位。
3. 各语言与平台的 log2 落地方式对比
3.1 标准库接口速查表
下面这张表是我自己在多语言项目里反复查抄的一份备忘,覆盖了浮点 log2 和整数 log2 两套接口。注意区分:带小数的 log2 属于浮点域,位运算属于整数域,两者不能混用。
| 语言/环境 | 浮点 log2 | 整数 floor(log2) | 备注 |
|---|---|---|---|
| C | log2(需 math 头文件) | 31 - __builtin_clz(x) | 参数 0 时内置函数行为未定义 |
| C++ | std::log2 | 同 C | 另有std::bit_width(C++20) |
| Java | Math.log无 log2,需自行处理 | 31 - Integer.numberOfLeadingZeros(x) | 长整型用Long.版本 |
| Python | math.log2 | n.bit_length() - 1 | 大整数也能算,位运算天然支持任意精度 |
| JavaScript | Math.log2 | 31 - Math.clz32(x)(仅 32 位) | 大整数可用BigInt转二进制串长度 |
| Rust | f64::log2 | u32::leading_zeros/ilog2 | ilog2对 0 会 panic |
| Go | math.Log2 | math/bits.Len32(x) - 1 | bits.Len对 0 返回 0 |
表格里最值得说的是“参数为 0”。几乎所有位运算版本的内置函数对 0 都没有良好定义:__builtin_clz(0)是未定义行为,可能返回 32 也可能返回任意值;Math.clz32(0)返回 32(这个是明确规定的);Rust 的ilog2直接 panic。所以整数 log2 的入口必须自己加一层守卫,这个守卫不能省,因为它不是“防御性编程”,而是“避免未定义行为”,性质完全不同。
3.2 整数域与浮点域:一条不能越过的分界线
我把这条当成铁律:输入是整数、输出也必须是整数时,全程不许碰浮点。
理由不是性能,而是正确性。浮点数的尾数位是有限的(双精度 53 位有效数字),当整数大到一定程度,log2 的结果里那点微小偏差会被完全吞掉。举个我自己验证过的例子:取 n = 2 的 48 次方加 1,这个整数在双精度里是能精确表示的(需要 49 位有效位,没超),但 log2 的真值是 48 加上一个约 1.4e-15 的小量,而 48 附近的浮点间隔(ulp)大约是 7.1e-15,也就是说这个超过 48 的部分连半个间隔都不到,四舍五入后结果就是精确的 48.0。你再去ceil,得到 48,但这个数其实需要 49 位才装得下。差一位,缓冲区就小了一格。
再看反方向:如果某次实现的误差让它落在 48.00000000000001,ceil变成 49,多分配一倍内存,性能莫名其妙掉一半。这种 bug 最难查,因为它是“大部分时候对”的,只在特定输入上错。
整数域的写法就没有这个问题,因为它根本不做近似:bit_length() - 1或最高位位置拿到的是精确解。代价是你得自己处理 0、负数和跨语言的位宽差异。这个代价我觉得非常划算。
3.3 从 IEEE 754 位模式中提取 log2 值
浮点数在内存里的排布本身就编码了 log2 的整数部分,这算是一个“结构上的巧合”,但用起来很实用。双精度是 1 位符号 + 11 位指数 + 52 位尾数,指数带 1023 的偏移;单精度是 1 位符号 + 8 位指数 + 23 位尾数,偏移 127。对于正常的正数,指数域减去偏移量,就是 floor(log2(x))。
这条性质支撑了一类很实用的技巧:把 x 的位模式当成整数读出来,除以 2 的尾数位数,再减偏移,就得到一个粗糙的 log2 近似,误差在 0.05 以内,只要几个周期。这个精度做实时的量级判断、数据分桶、感知哈希足够了。要更准就在尾数上补一段多项式修正。
有个坑必须点出来:非规格化数(denormal)失效。这类数的指数域是全 0,尾数不再是隐含 1 开头,上面那套推导直接崩掉。处理办法是先乘一个 2 的整数次幂把数拉回正常范围,算完再减去这个幂次。另一个坑是负数、零、无穷大和 NaN,它们要么指数域全 1,要么没有意义,必须提前挡掉。
我在一个音频处理的小工具里用过这招做电平分级,效果不错,但后来发现输入信号偶尔会掉到非规格化区间(长时间静音后淡入),分级就乱了。加上“小于某个极小值直接归零档”的判断之后才稳。这类边界在实践中出现的频率远比教科书说的要高。
4. 容量规划中的 log2 算例:从位数到结构深度
4.1 需要多少位才能装下 N 种状态
这是 log2 最基础的应用:N 种状态需要 ceil(log2(N)) 位。N 等于 1 时是 0 位(不需要区分),N = 2 是 1 位,N = 3 到 4 是 2 位,N = 5 到 8 是 3 位。注意这里必须是向上取整,向下取整会少一位,直接导致状态装不下。
整数版的向上取整写法有个很漂亮的位技巧,比浮点版本稳得多:
// 返回装下 n 种状态所需的最少位数,n >= 1 static inline unsigned ceil_log2_u32(uint32_t n) { if (n <= 1) return 0; // 1 种状态不需要位 return 32u - __builtin_clz(n - 1); // 先减 1 再数前导零 }先减 1 是关键:如果 n 本身就是 2 的幂,直接对 n 取整会多算一位。n = 8 时,n-1 = 7,前导零 29 个,32 - 29 = 3,正好;如果不对 n 减 1 而是对 8 求,前导零 28 个,得到 4,多了一位。这个“减一”就是处理“已经是 2 的幂”这个边界,我在代码评审里见过太多次漏掉它。
Python 里同样优雅:(n - 1).bit_length(),n 必须大于 1。JavaScript 的 32 位版本是32 - Math.clz32(n - 1),但要注意Math.clz32的参数会被转成 32 位无符号整数,超过 2 的 32 次方就溢出了,得换BigInt路线。
4.2 索引层数与哈希表扩容次数估算
我做过一个数据检索模块的容量评审,用 log2 把整个链路的层数算了一遍,结论直接改变了分片策略,所以这个算例我觉得值得完整写出来。
假设单页 16KB,主键 8 字节,子页指针 6 字节,一条索引项 14 字节,加上页头页尾开销,有效载荷按 14KB 算,那单页能放 14000 / 14 = 1000 条,分叉数就是 1000。数据量按一亿条算:
| 层级 | 单层容量(累计) | 计算式 |
|---|---|---|
| 叶子层 | 1e3 条记录 | 1000 |
| 加一层内部节点 | 1e6 条 | 1000 的 2 次方 |
| 再加一层 | 1e9 条 | 1000 的 3 次方 |
| 再加一层 | 1e12 条 | 1000 的 4 次方 |
一亿条落在 1e6 和 1e9 之间,所以需要 3 层。换算成 log2 语言就是:层数 = log(1e8) 以 1000 为底 = 8/3 ≈ 2.67,向上取整 3。这里 log2 的角色是“把分叉数换成 2 的等价层数”——1000 叉树走一层,相当于二叉树走 log2(1000) ≈ 9.97 层,也就是约 10 层。
这个换算特别有用,因为它让你能在“多叉”和“二分”之间建立统一尺度。你听到“三层 B+ 树”,脑子里应该立刻转成“等价于二叉查找树的十层”,然后立刻意识到:一次查询最多 3 次磁盘读,等价于 10 次比较的深度。这个数量级判断能力,比记具体公式有用得多。
哈希表的扩容次数同理。初始 16 个桶,每次翻倍,涨到 1048576 个桶要扩容 log2(1048576 / 16) = log2(65536) = 16 次。每次扩容都要把所有元素重新映射一遍,总工作量是等比数列求和,约等于最后一次扩容的两倍。这就是为什么“预设容量”能显著减少抖动——你只要算一次 log2,就能把 16 次全量重排省掉。
4.3 信息量与音频倍频程:log2 的两种实用场景
信息量这块,log2 是定义的一部分。一个事件的信息量是 -log2(p) 比特,p 是它的概率。均匀分布的 8 种可能,每种 1/8,信息量 log2(8) = 3 比特,正好对应 3 位二进制。熵就是所有事件信息量的期望值,单位也是比特。
这个定义在实际工作里很能打。比如评估一个分类模型的输出质量:平均交叉熵是 2.3 比特,那困惑度就是 2 的 2.3 次方 ≈ 4.9,意思是模型的表现大约相当于在 5 个候选里犹豫。这种把“概率”翻译成“候选个数”的能力,让指标一下子变得可感知。我在做文本处理的时候,经常把交叉熵直接换算成“平均每个字符几个比特”,然后跟原始文件的压缩率对照,很快就能判断模型是否真的学到了结构。
音频和信号这边,log2 用来数倍频程:两个频率相差一个八度,就是 log2(f2/f1) = 1。441Hz 到 882Hz 是一个八度,到 1764Hz 是两个八度。人耳对频率的感知接近对数刻度,所以均衡器、频谱分析、滤波器组的设计都按倍频程来划分档位,档位数自然就是 log2 出来的。
这里有个常见混淆必须提醒:振幅比和功率比差一个平方,所以换算成分贝时系数分别是 20 和 10。我见过有人把功率比按 20 倍系数算,结果整体偏了一倍分贝值,排查了半天以为是采集卡的问题。记住这条:算电压、电流、声压这类“幅度量”用 20,算功率、能量、强度用 10。而“每减半降多少分贝”这个问题,答案对幅度是约 -6dB,对功率是约 -3dB,因为 log2(0.5) = -1,乘上系数就出来了。
5. 手写一套可复用的 log2 工具函数
5.1 接口设计与约定
零散的 log2 调用是这个模块最大的维护负担,所以我习惯把它收敛成一小套工具函数,接口约定写死在注释里:整数版只接受大于 0 的整数,越界抛异常或返回约定值;浮点版接受大于 0 的浮点,其余返回 NaN 或抛异常。返回值语义统一为“向下取整的整数”和“连续浮点值”两种,命名上区分清楚,避免调用方猜。
我特别强调一点:不要给整数版做“自动容错”。有人喜欢在参数为 0 时返回 0 或 -1 糊弄过去,这会让调用方永远发现不了上游传了 0。宁可让它在边界上炸一次,把问题暴露在测试阶段。生产代码里真正的保护应该放在调用侧的输入校验,而不是把错误吞掉。
5.2 整数版实现:32 位、64 位与可移植回退
#include <stdint.h> // 前置条件: x > 0。返回 floor(log2(x)) static inline int ilog2_u32(uint32_t x) { return 31 - __builtin_clz(x); } static inline int ilog2_u64(uint64_t x) { return 63 - __builtin_clzll(x); } // 需要在没有内置函数的平台上编译时用的回退版本 static inline int ilog2_u32_portable(uint32_t x) { int r = 0; if (x >= 0x10000u) { x >>= 16; r += 16; } if (x >= 0x100u) { x >>= 8; r += 8; } if (x >= 0x10u) { x >>= 4; r += 4; } if (x >= 0x4u) { x >>= 2; r += 2; } if (x >= 0x2u) { r += 1; } return r; }回退版本用二分判定:先看能不能右移 16 位,再看 8 位、4 位、2 位、1 位,五次判断覆盖 32 位,分支预测友好的时候也就十来个周期。这个写法比循环移位快,而且没有任何未定义行为,适合交叉编译到工具链支持不全的平台。我用它跑过一轮对比,在无法生成位扫描指令的情况下,比while循环版本快两三倍。
64 位版本要注意一个坑:如果平台是 32 位,__builtin_clzll可能被拆成两次 32 位操作,性能未必比手写快。这时候可以先用uint32_t的高位判断一次,再决定走哪条路径:
static inline int ilog2_u64_split(uint64_t x) { uint32_t hi = (uint32_t)(x >> 32); if (hi) return 32 + ilog2_u32(hi); return ilog2_u32((uint32_t)x); }这个写法在混合位宽的场景下更稳,也更容易让编译器生成干净的分支。
5.3 浮点版与精度自检
浮点版我基本不自己写,直接用标准库,但必须加一层“整数结果校验”。因为工程里最常见的需求是“算出来应该是个整数”,那就检验它到底是不是:
import math def log2_int_exact(n: int) -> int: """n 必须是正整数。要求 log2(n) 是整数,否则抛错。 用整数域计算,永不产生浮点误差。""" if n <= 0: raise ValueError("n must be positive") k = n.bit_length() - 1 if (1 << k) != n: raise ValueError(f"{n} is not a power of two") return k def log2_float_checked(x: float) -> float: """浮点 log2,附带 2 的幂自检,用于定位精度问题。""" if not (x > 0.0): raise ValueError("x must be > 0") v = math.log2(x) r = round(v) # 如果结果接近整数,验证 2**r 是否真的等于 x,据此判断底层是否有偏差 if abs(v - r) < 1e-9 and abs(r) < 1000: if 2.0 ** r != x: raise ArithmeticError(f"suspicious log2({x}) = {v!r}, expected {r}") return v这段自检逻辑我在多个项目里都留着,直接抄就行。它的作用是:平时什么都不做,一旦某个版本的语言运行时或数学库在 2 的幂上给出 29.000000000000004 这种结果,它会在测试阶段立刻报出来,而不是等到线上算出少一页数据。老版本 JS 引擎里Math.log2就出现过这种偏差(当时是用自然对数乘常数实现的),依赖引擎版本判断不如自己校验来得踏实。
5.4 基准数据与选型结论
我在本机做过一轮粗略对比(x86-64、开优化、百万次循环取平均),数据只是量级参考,不同机器和编译器会差不少:
| 实现方式 | 相对开销(量级) | 精度 | 适用场景 |
|---|---|---|---|
std::log2(double) | 10~40 周期 | 高(近似正确舍入) | 冷路径、需要小数结果 |
__builtin_clz系列 | 1~2 周期 | 精确(整数) | 热路径、容量/位宽计算 |
| 二分回退版 | 10~20 周期 | 精确(整数) | 无内置函数的平台 |
| 位模式近似 | 3~5 周期 | 误差约 0.05 | 分级、分桶、粗略判断 |
选型结论很直接:整数需求一律走位运算,需要小数才用浮点库函数。如果浮点结果还要立刻转成整数,那基本可以判定写法有问题,重写成整数域版本。唯一例外是输入本身就是浮点(比如信号幅度、频率比、概率),那用浮点库没问题,但要留意 1 附近和非规格化数这两个敏感区。
6. 踩坑实录:log2 相关问题速查
6.1 精度与边界问题表
这张表是我从几个项目的问题单里整理出来的,按“症状→根因→修法”的顺序排,遇到类似现象可以直接对号。
| 症状 | 根因 | 修法 |
|---|---|---|
| 某数据量下少算一页/一位 | log(x)/log(2)加ceil的舍入偏差 | 改用整数位运算求位数 |
| 结果出现 28.999999999999996 | 换底公式两次舍入 | 直接用独立的 log2 接口 |
| 容量比需要的大一倍 | 对 2 的幂没做“减一”处理 | ceil前先n - 1 |
| 参数为 0 时随机结果 | 内置前导零函数对 0 未定义 | 入口加x > 0守卫 |
| 极小信号分级错乱 | 非规格化数导致位模式取值失效 | 先乘 2 的幂归一化,再减回去 |
| 幅度/功率分贝值差一倍 | 系数 20 和 10 用混 | 幅度用 20,功率用 10 |
| 大整数输入结果错位 | 输入超出浮点尾数精度 | 全程整数域计算 |
表里第一行和第二行是最常见的,两者经常一起出现。我后来养成了一个习惯:只要在代码里看到log、log2、log10后面跟着ceil、floor、int()、Math.round,就一定要停下来问一句“这里的输入是不是整数”。是整数,就要求改写成位运算;不是整数,那就再问一句“1 附近会不会取到”,如果会,加上区间保护。
6.2 性能与可移植性问题
性能上真正的问题不是 log2 本身慢,而是它出现在不该出现的位置。我接手过一个日志采样模块,每写一条日志都要算一次log2(seq)来做指数退避,一秒钟几万条日志,全部走浮点库,profile 里这个函数占了 8% 的样本。改成“只在 seq 是 2 的幂时才算下一次退避时间,并用整数位运算”之后,这部分直接降到噪声级别。改法很简单:退避序列本来只关心“当前在第几档”,而第几档就是最高有效位位置,不需要真的算对数值。
可移植性上要注意三点。第一,32 位和 64 位的位宽差异,写通用函数时必须显式区分,别指望类型自动推导。第二,某些嵌入式工具链没有提供前导零计数指令,__builtin_clz会退化成调用库函数,性能反而更差,这种平台直接用手写的二分回退版更踏实。第三,不同语言的整数 log2 对 0 的处理完全不同(有的未定义,有的返回 0,有的抛异常),跨语言移植的时候这一条必须逐个确认,不能想当然。
6.3 我个人的几条硬性规则
最后分享几条我自己写代码时严格执行的规矩,都是问题上换来的。
第一条,整数 log2 只写一次,写在一个地方。每多一个地方手写log2,就多一个漏掉边界的机会。收敛成一个工具函数,测试覆盖 0、1、2、2 的幂、2 的幂加一、最大值这几组,之后所有调用都走它。
第二条,向上取整永远用“先减一”的写法。不管是算位数、算页数、算层数,ceil_log2(n)内部一律先算n - 1,这样 n 恰好是 2 的幂时不会多算一位。这个细节我写过太多次,闭着眼也不会错。
第三条,浮点 log2 的结果不要做精确相等判断。if log2(x) == 3这种写法在任何语言里都不安全,要写成“四舍五入后验证 2 的幂是否等于原值”,或者干脆改走整数域。我在评审里看到精确相等判断,基本都会标红。
第四条,涉及单位换算的地方,把系数和公式写进注释。分贝的 20/10、倍频程的 log2、信息量的比特,这些地方一旦出错,结果看起来往往是“差不多对”的,非常难发现。把“这个数是振幅比还是功率比”“这个结果单位是比特还是位”直接写在代码旁边,比事后写文档管用。
这几条听起来琐碎,但每一条都对应过一次真实的、花了半天以上才定位的偏差。log2 函数本身没有多复杂,复杂的是它出现的位置往往离真实业务很远——你在算容量,出错却表现在“最后一条数据查不出来”;你在算分贝,出错却表现在“音量调节曲线不顺滑”。正是这种距离感,让它的每一次误用都特别难查。所以与其追求写得聪明,不如守规矩写得笨。