bigywater.tech
Rust · 41

静态分发与动态分发性能对比

「动态分发更慢」这句话对不对?这篇用同一个遍历循环做四种对照:全是同一种类型、规律交替、随机排列、改用 enum。结论是:动态分发的代价不在「查 vtable」本身,而在分支预测与无法内联 —— 目标可预测时几乎免费,不可预测时慢好几倍。

5.6 倍
dyn 随机排列相对静态分发的每对象耗时
0.93 vs 0.90 ns
dyn 全是同一种类型时与静态分发几乎一样
ldr + br
dyn 调用在 aarch64 上的全部指令:取 vtable 槽、跳转

四种遍历的耗时

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    泛型/静态分发 Vec<Circle>(可内联、可向量化)      每个对象 0.90 ns
    动态分发 Vec<Box<dyn Shape>>,全是 Circle           每个对象 0.93 ns
    动态分发,三种类型按 0,1,2,0,1,2 规律交替             每个对象 2.30 ns
    动态分发,三种类型随机排列(分支预测失败)            每个对象 5.06 ns
    enum 分发 Vec<AnyShape>(match,可内联)             每个对象 0.86 ns

基准用 Instant 计时、release 构建,每种配置取 20 次中位数;这个循环本身受内存访问影响很大,所以静态分发没有显著领先于全是同一类型的 dyn。数字只对这台机器与这个负载有意义。

汇编里的差别

实测 · 本机执行输出
  static_call(&Circle) 的汇编: ldr	d0, [x0] ; mov	x8, #34414 ; movk	x8, #61467, lsl #16 ; movk	x8, #8697, lsl #32 ; movk	x8, #16393, lsl #48 ; fmov	d1, x8 ; fmul	d1, d0, d1 ; fmul	d0, d0, d1 ; ret
  dyn_call(&dyn Shape) 的汇编: ldr	x1, [x1, #24] ; br	x1

对象怎么布局、胖指针里有什么,见Trait Object 与动态分发;单态化的代码体积代价见泛型单态化。

分步演示

静态与动态分发
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。release 构建,Apple 芯片;随机排列用固定种子的线性同余生成器
    • 没有验证:x86 上的间接分支预测表现、为何静态分发 Vec 没有比 dyn 同类型更快(我猜是内存带宽受限,没有用性能计数器验证)、PGO 对 dyn 调用的去虚化(devirtualization)、对象很大时缓存未命中的影响
    京ICP备2026067502号-1京公网安备11010502063487号