性能瓶颈主要在内存分配和方法调用模式:频繁 new(big.Int) 触发 GC,Exp/Mod 等方法未复用接收器导致重复分配与缓冲区重建,正确做法是预声明 *big.Int 变量并用 Set 重置,避免隐式分配。

Go 语言用 math/big 做加密运算时,性能瓶颈几乎总是出在内存分配和方法调用模式上,而不是算法本身。直接套用文档示例写法,在 RSA 或 ECC 场景下很容易慢 3–5 倍。
为什么 big.Int 在加密循环里越跑越慢
加密算法(比如 RSA 的模幂、ECDSA 的点乘)通常要执行成百上千次 Add、Mul、Exp、Mod 等操作。每次调用 new(big.Int) 或 big.NewInt(0) 都会触发堆分配;而 big.Int 内部的 abs 字段是 []big.Word(底层是 []uint),频繁扩容 + GC 会显著拖慢吞吐。
- 典型现象:1024 位 RSA 私钥解密耗时从 80μs 涨到 300μs,Profile 显示
runtime.mallocgc占比超 40% -
Exp和Mod方法内部也会临时创建多个*big.Int,除非你显式传入预分配的接收器 - 没复用对象时,
a.Mul(a, b)看似简洁,但若a后续还要参与其他计算,就等于反复覆盖+重建缓冲区
复用 *big.Int 对象的正确姿势
加密场景下,固定尺寸的中间变量极多(比如模数 n、底数 base、临时余数 r、平方缓存 sqr)。把这些声明为局部变量或结构体字段,用 Set 或 SetInt64 重置,比每次都 new 快得多。
- 别写:
res := new(big.Int).Exp(base, exp, mod)—— 每次都新分配 - 改写:
res.Exp(base, exp, mod),其中res是已声明的*big.Int变量 - 批量运算时,用
sync.Pool管理临时对象,但取出后必须调z.Set(nil)或z.SetInt64(0),否则残留数据会导致计算错误 - 对固定长度的大数(如 2048 位 RSA 参数),可预先用
z.Bits()估算所需字长,再用z.abs = make([]big.Word, cap)手动预分配(不常用,但极端场景有效)
Mod/Exp/QuoRem 这些方法的接收器陷阱
加密中高频使用的模幂 Exp(x, y, m)、模除 QuoRem、快速约简等,都要求你明确指定哪个变量接收结果。漏掉接收器或传错顺序,轻则结果错,重则 panic。
-
z.Exp(x, y, m):结果写入z,x和y不变;但若误写成x.Exp(x, y, m),x就被覆盖了,后续签名验签全乱 -
q, r := new(big.Int).QuoRem(a, b, new(big.Int))—— 第三个参数是余数接收器,不是商;商始终返回第一个参数(即q),余数写入第三个 - 模幂中
m为nil表示无模,但加密里m绝不能为nil,否则变成纯幂运算,2^65537 直接爆内存 - 所有
Mod类方法对负数的处理是向零截断,RSA 中若输入为负(比如某些 padding 处理后),需先Mod再Exp,顺序不能反
字符串解析和二进制导入的隐蔽开销
从 PEM 或 DER 解析密钥时,常把 Base64 解码后的字节切片转成 *big.Int。这时别用 SetString(hex.EncodeToString(bs), 16),绕远路且易出错。
- 直接用
new(big.Int).SetBytes(bs),它按大端序解析字节流,和 ASN.1/DER 编码完全一致 -
SetString默认十进制,十六进制必须显式传16,且字符串不能带0x前缀或空格;PEM 密钥里常见换行符,strings.TrimSpace必须加 - 从 JSON 读取十六进制字符串(如
"0xabc123")时,先去掉前缀再调SetString(s[2:], 16),否则静默失败(ok == false) - 避免用
fmt.Sprintf("%x", n)再转回,大数转字符串本身就有开销,加密中应尽量保持二进制路径
最易被忽略的是:即使你复用了所有 *big.Int,如果没控制好 Exp 的算法参数(比如没启用 Montgomery 优化),或者在循环中反复调 BitLen() 判断位宽,性能照样垮。这些细节不报错,但会让本该跑在纳秒级的操作卡在微秒级。



















