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 上,性能可相差两个数量级。是否使用硬件加速、是否向量化、是否利用多核并行,直接决定了优化的上限。

密码性能瓶颈来源与优化作用点 瓶颈来源 算法数学结构 轮函数复杂度 / 模幂位数 点乘次数 / 密钥长度 数据依赖与分支 密钥相关分支 / 查表地址 流水线停顿 / 侧信道风险 内存访问模式 大表查表 / 缓存未命中 多核缓存竞争 / 抖动 硬件与指令集 AES-NI / SHA 扩展 / AVX 多核并行 / 嵌入式算力 优化作用点 算法选择与参数 ECC 替代 RSA / 会话密钥长度 批量签名 / 预计算 常数时间实现 消除密钥分支 / 掩码查表 位运算替代条件判断 数据布局与缓存 位切片 / 小表 / 预取 SIMD 向量化 / 批处理 硬件加速与并行 AES-NI / 多线程 / 流水线 GPU / 专用加速卡 工程约束 安全性不可妥协 常数时间 / 抗侧信道 密钥零化 / 防故障注入 兼容性与标准 协议固定算法 / 互操作 FIPS / 国密合规 资源与功耗 内存占用 / 代码体积 功耗预算 / 散热限制 可维护与可移植 跨平台 / 可测试 代码可读性 / 审计 虚线:优化方案受工程约束反向制约

二、优化目标不是“越快越好”

性能优化有多个维度,不同场景下优先级完全不同。常见的度量指标包括:

  • 吞吐量(Throughput):单位时间内能处理的明文或签名数量,单位常为 MB/s 或 ops/s。适合批量加密、TLS 记录层处理等场景。
  • 延迟(Latency):单次操作从输入到输出的耗时,单位常为微秒或毫秒。适合交互式登录、单次握手等场景。
  • 资源占用:内存峰值、代码体积、寄存器压力。嵌入式设备上,RAM 和 Flash 往往比 CPU 更稀缺。
  • 功耗与能量:每次运算消耗的能量,直接影响电池寿命和散热设计。
  • 常数时间性:执行时间是否与密钥、明文相关。这是安全指标,但在工程上表现为“性能不能忽快忽慢”。

这些目标之间存在张力。例如,用大表查表可以提升 AES 吞吐,但会增大内存占用并引入缓存侧信道;用多线程并行可以提升吞吐,但会增加延迟抖动和同步开销。因此,优化的第一步不是写代码,而是明确当前系统的主导指标和硬约束。

三、工程约束:性能优化的边界

密码性能优化与普通程序优化最大的区别在于:安全性是不可谈判的约束。任何优化都不能引入新的侧信道、不能降低密钥强度、不能破坏协议兼容性。具体来说,工程约束通常包括:

  • 常数时间要求:涉及密钥的分支、查表、循环次数必须与秘密数据无关。这意味着不能简单地“用 if 选择更快路径”。
  • 标准与合规:TLS、IPSec、国密等协议规定了算法套件和参数,不能随意替换。优化只能在实现层面进行。
  • 互操作性:优化后的实现必须与标准实现产生相同结果,包括填充、编码、字节序等细节。
  • 可移植性:同一份代码可能运行在 x86、ARM、RISC-V 上,过度依赖特定指令集会降低可移植性。
  • 可验证性:优化后的代码仍需通过测试向量、模糊测试和侧信道评估。
优化前先问三个问题:当前瓶颈是吞吐还是延迟?安全约束是否允许改变数据流?目标平台有哪些可用指令和缓存特性?这三个问题的答案,决定了优化方向。

四、建立性能度量与优化思维

性能优化不是凭感觉调参,而是一个“度量—定位—假设—验证”的闭环。建议按以下顺序推进:

  1. 建立基线:在目标平台上测量当前实现的吞吐、延迟、内存占用。基线必须可复现,记录 CPU 型号、频率、编译器版本、编译选项、输入规模。
  2. 定位热点:用 profiler 找到耗时最集中的函数或指令段。密码运算的热点通常非常集中,例如模幂中的 Montgomery 乘法、AES 中的轮函数。
  3. 提出假设:例如“模幂耗时主要来自模乘的进位处理”或“AES 查表导致 L1 缓存未命中率偏高”。
  4. 小步验证:每次只改一个变量,测量变化。避免一次性重写整个模块,否则无法归因。
  5. 回归安全测试:优化后必须重新跑测试向量和侧信道检查,确认没有引入正确性或安全性问题。

以一个具体示例说明。假设在 x86-64 平台上测试 OpenSSL 的 RSA-2048 签名,基线为每秒 1200 次签名,单次延迟约 0.83 ms。用 perf 定位后发现,约 70% 的时间花在 bn_mul_mont 函数上。进一步观察发现,该函数使用了 64 位乘法但未充分利用 ADX 指令的进位链。假设“使用 ADX 双进位链可以减少进位传播开销”,于是启用 -DOPENSSL_IA32CAP 相关编译选项并替换模乘实现。重新测量后,签名吞吐提升到约每秒 1800 次,延迟降至约 0.56 ms。这个提升来自指令级优化,而非算法替换,安全性没有变化。注意,以上数值为教学示例,实际结果取决于具体 CPU 和编译环境。

不要在没有基线的情况下优化。没有度量,就无法判断优化是否有效,也无法发现优化引入的性能回退。每次优化后都应保留可对比的基准数据。

五、本章要点