1. 密码性能优化导论
在密码工程实践中,一个算法“能算对”与“能上线”之间往往隔着巨大的性能鸿沟。AES-128 在通用 CPU 上单核吞吐可达每秒数 GB,而同样安全的某些后量子签名方案,单次验签可能耗时数毫秒;同样是 RSA-2048,使用不同的大数库、不同的模幂实现,签名速度可能相差数倍。密码性能优化,研究的正是如何在不削弱安全性的前提下,让密码运算在真实硬件、真实协议、真实业务负载下跑得更快、更稳、更省资源。
一、性能瓶颈从哪里来
密码算法的耗时并非均匀分布,它通常集中在少数几个“热点”上。理解瓶颈来源,是优化的第一步。可以把瓶颈大致归为四类。
第一类是算法本身的数学结构。对称密码的核心是轮函数,AES 的 SubBytes、MixColumns 决定了每字节需要多少次查表或位运算;公钥密码的核心是大数模幂或椭圆曲线点乘,其复杂度随密钥长度呈多项式甚至超线性增长。RSA-2048 的模幂涉及约 2048 位整数的上千次模乘,而 ECC-256 的点乘只需约 256 次点加与倍点,这就是为什么在同等安全强度下 ECC 的密钥更短、运算更快。
第二类是数据依赖与分支。密码实现中大量存在“根据密钥或中间值决定下一步”的逻辑。例如朴素模幂中的平方-乘算法,若按密钥比特决定是否执行乘法,就会产生分支;AES 的 S 盒若用查表实现,表地址与密钥相关,会引发缓存访问模式差异。这类数据依赖不仅拖慢流水线,更会带来侧信道风险,因此常需要改造成常数时间实现,而常数时间往往意味着牺牲一部分峰值性能。
第三类是内存访问模式。现代 CPU 的算力远高于内存带宽,密码运算若频繁访问大表或随机地址,就会受制于缓存未命中。AES 的 T 表实现(4 张 1KB 表)在早期 CPU 上很快,但在多核共享缓存场景下,表竞争和缓存抖动会显著降低吞吐。相比之下,AES-NI 指令把轮函数固化进硬件,彻底绕开了查表瓶颈。
第四类是硬件与指令集能力。同一份算法,在支持 AES-NI、SHA 扩展、AVX-512 的 CPU 上,与在仅支持基础指令的嵌入式 MCU 上,性能可相差两个数量级。是否使用硬件加速、是否向量化、是否利用多核并行,直接决定了优化的上限。
二、优化目标不是“越快越好”
性能优化有多个维度,不同场景下优先级完全不同。常见的度量指标包括:
- 吞吐量(Throughput):单位时间内能处理的明文或签名数量,单位常为 MB/s 或 ops/s。适合批量加密、TLS 记录层处理等场景。
- 延迟(Latency):单次操作从输入到输出的耗时,单位常为微秒或毫秒。适合交互式登录、单次握手等场景。
- 资源占用:内存峰值、代码体积、寄存器压力。嵌入式设备上,RAM 和 Flash 往往比 CPU 更稀缺。
- 功耗与能量:每次运算消耗的能量,直接影响电池寿命和散热设计。
- 常数时间性:执行时间是否与密钥、明文相关。这是安全指标,但在工程上表现为“性能不能忽快忽慢”。
这些目标之间存在张力。例如,用大表查表可以提升 AES 吞吐,但会增大内存占用并引入缓存侧信道;用多线程并行可以提升吞吐,但会增加延迟抖动和同步开销。因此,优化的第一步不是写代码,而是明确当前系统的主导指标和硬约束。
三、工程约束:性能优化的边界
密码性能优化与普通程序优化最大的区别在于:安全性是不可谈判的约束。任何优化都不能引入新的侧信道、不能降低密钥强度、不能破坏协议兼容性。具体来说,工程约束通常包括:
- 常数时间要求:涉及密钥的分支、查表、循环次数必须与秘密数据无关。这意味着不能简单地“用 if 选择更快路径”。
- 标准与合规:TLS、IPSec、国密等协议规定了算法套件和参数,不能随意替换。优化只能在实现层面进行。
- 互操作性:优化后的实现必须与标准实现产生相同结果,包括填充、编码、字节序等细节。
- 可移植性:同一份代码可能运行在 x86、ARM、RISC-V 上,过度依赖特定指令集会降低可移植性。
- 可验证性:优化后的代码仍需通过测试向量、模糊测试和侧信道评估。
四、建立性能度量与优化思维
性能优化不是凭感觉调参,而是一个“度量—定位—假设—验证”的闭环。建议按以下顺序推进:
- 建立基线:在目标平台上测量当前实现的吞吐、延迟、内存占用。基线必须可复现,记录 CPU 型号、频率、编译器版本、编译选项、输入规模。
- 定位热点:用 profiler 找到耗时最集中的函数或指令段。密码运算的热点通常非常集中,例如模幂中的 Montgomery 乘法、AES 中的轮函数。
- 提出假设:例如“模幂耗时主要来自模乘的进位处理”或“AES 查表导致 L1 缓存未命中率偏高”。
- 小步验证:每次只改一个变量,测量变化。避免一次性重写整个模块,否则无法归因。
- 回归安全测试:优化后必须重新跑测试向量和侧信道检查,确认没有引入正确性或安全性问题。
以一个具体示例说明。假设在 x86-64 平台上测试 OpenSSL 的 RSA-2048 签名,基线为每秒 1200 次签名,单次延迟约 0.83 ms。用 perf 定位后发现,约 70% 的时间花在 bn_mul_mont 函数上。进一步观察发现,该函数使用了 64 位乘法但未充分利用 ADX 指令的进位链。假设“使用 ADX 双进位链可以减少进位传播开销”,于是启用 -DOPENSSL_IA32CAP 相关编译选项并替换模乘实现。重新测量后,签名吞吐提升到约每秒 1800 次,延迟降至约 0.56 ms。这个提升来自指令级优化,而非算法替换,安全性没有变化。注意,以上数值为教学示例,实际结果取决于具体 CPU 和编译环境。