big.int频繁new是gc压力飙升的头号原因;应复用变量、sync.pool配合z.set(nil)重置、预分配abs底层数组,并避免exp/mod等方法误用接收器。

big.Int 频繁 new 导致 runtime.mallocgc 暴涨
GC 压力飙升的头号原因,就是循环里写 new(big.Int) 或 big.NewInt(0)。pprof 一看 runtime.mallocgc 占 CPU 40%+,基本可以锁定它。
每次 new 都分配堆内存,big.Int 内部的 abs 字段是 []big.Word(即 []uint64),扩容 + GC 双重开销。实测在 1024 位模幂运算中,复用 vs 每次 new,耗时差 3–5 倍(80μs → 300μs)。
- 错误写法:
res := new(big.Int).Exp(base, exp, mod)—— 每次都新对象,还立刻丢弃 - 正确写法:
res.Exp(base, exp, mod),其中res是已声明的*big.Int变量 - 局部变量声明即可复用:
var res big.Int,后续用res.SetInt64(0)或res.Set(nil)重置
sync.Pool 复用 big.Int 必须调用 Set(nil)
sync.Pool 能缓存 *big.Int,但取出后不清理状态,等于喂 GC 垃圾——旧 abs 底层数组仍被引用,无法回收。
Pool 的 New 函数返回对象后,必须显式调用 z.Set(nil) 或 z.SetInt64(0),否则残留数据会导致计算错误。只清长度(z.abs = z.abs[:0])不够,Set(nil) 才真正释放底层数组引用。
- 示例:
var intPool = sync.Pool{New: func() interface{} { return new(big.Int) }} - 取用时:
z := intPool.Get().(*big.Int),用完必须defer intPool.Put(z.Set(nil)) - 别写
intPool.Put(z)或intPool.Put(z.SetInt64(0))—— 后者没清底层数组,前者残留引用
Exp/Mod/QuoRem 等方法的接收器陷阱
这些方法不返回新对象,而是把结果写入调用者自身(接收器)。传错接收器或覆盖原值,轻则结果错,重则 panic,且掩盖内存问题。
-
z.Exp(x, y, m):结果写入z,x和y不变;误写成x.Exp(x, y, m),x就被覆盖了 -
q, r := a.QuoRem(b, r):第三个参数r是余数接收器,商始终返回第一个返回值q,不是r -
Mod对负数向零截断,RSA 中若 padding 后为负,必须先Mod再Exp,顺序反了结果非法 -
m在Exp中为nil表示无模——但加密中绝不能为nil,否则2^65537直接爆内存
固定位宽大数可预分配 abs 底层数组
RSA 2048、ECC secp256k1 等固定位宽场景下,手动预分配 abs 能避免运行时多次扩容,进一步压低 GC 峰值。
先估算字长:cap := (bitLen + 63) / 64(big.Word 是 uint64),再预分配:z.abs = make([]big.Word, cap)。之后所有 Set/Exp 都复用该底层数组。
- 这只在位宽高度稳定时有效(如密钥参数),动态长度大数不适用
- 预分配后仍需配合
Set或SetInt64清零,不能跳过重置步骤 - 不建议盲目预分配:若位宽波动超过 ±128 bit,反而增加内存浪费和 cache miss
最易被忽略的是接收器复用与 Set(nil) 的组合动作——不是“用了 Pool 就安全”,也不是“声明了变量就万事大吉”。每个 big.Int 实例背后都有一个可增长的底层数组,而它的生命周期由你显式控制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











