You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust泛型与单态代码性能差异排查:为何泛型版本更慢?

Rust泛型与手动单态版本的性能差异问题

问题描述

我在学习Rust时,用criterion做基准测试发现,泛型版本的build_set::<f64>性能比手动编写的单态版本build_set_f64慢一倍左右,有两个疑问:

  1. 我原以为泛型代码编译时会被单态化,性能应该和单态版本接近,是不是我的理解有误?
  2. 会不会是向量的读写操作存在疏漏导致性能差异?

基准测试结果

monomorphic/build_set_f64: time:   [4.7040 µs 4.8005 µs 4.9150 µs]
generic/build_set::<f64>:  time:   [9.6551 µs 9.7832 µs 9.9279 µs]

相关代码

核心逻辑代码

use num_traits::{Num};
use num_traits::identities::{one, zero};

#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub struct H2H<T> {
  pub vs1: T,
  pub vs2: T
}

pub struct Set<T> {
  pub vec: Vec<H2H<T>>,
  pub min_win: usize
}

pub fn build_set<T: Num+Copy>(min_win: usize, psrv: T, p1: T, p2: T) -> Set<T> {
  let mut vec: Vec<H2H<T>> = vec![H2H {vs1: zero(), vs2: zero()}; (min_win+1) * (min_win+1)];

  vec[0] = H2H {vs1: psrv, vs2: one::<T>()-psrv};

  for vs1 in 0..min_win+1 {
    for vs2 in 0..min_win+1 {
      if 0 < vs1 && vs1 <= min_win && vs2 < min_win {
        let prv = vec[index_at_score(min_win, vs1-1, vs2)];

        vec[index_at_score(min_win, vs1, vs2)].vs1 = prv.vs1 * p1 + prv.vs2 * (one::<T>() - p2);
      }

      if 0 < vs2 && vs2 <= min_win && vs1 < min_win {
        let prv = vec[index_at_score(min_win, vs1, vs2-1)];

        vec[index_at_score(min_win, vs1, vs2)].vs2 = prv.vs1 * (one::<T>() - p1) + prv.vs2 * p2;
      }
    }
  }

  return Set { vec, min_win };
}

pub fn build_set_f64(min_win: usize, psrv: f64, p1: f64, p2: f64) -> Set<f64> {
  let mut vec: Vec<H2H<f64>> = vec![H2H {vs1: zero(), vs2: zero()}; (min_win+1) * (min_win+1)];

  vec[0] = H2H {vs1: psrv, vs2: one::<f64>()-psrv};

  for vs1 in 0..min_win+1 {
    for vs2 in 0..min_win+1 {
      if 0 < vs1 && vs1 <= min_win && vs2 < min_win {
        let prv = vec[index_at_score(min_win, vs1-1, vs2)];

        vec[index_at_score(min_win, vs1, vs2)].vs1 = prv.vs1 * p1 + prv.vs2 * (one::<f64>() - p2);
      }

      if 0 < vs2 && vs2 <= min_win && vs1 < min_win {
        let prv = vec[index_at_score(min_win, vs1, vs2-1)];

        vec[index_at_score(min_win, vs1, vs2)].vs2 = prv.vs1 * (one::<f64>() - p1) + prv.vs2 * p2;
      }
    }
  }

  return Set { vec, min_win };
}

pub fn index_at_score(min_win: usize, vs1: usize, vs2: usize) -> usize {
  return vs1 * (min_win + 1) + vs2;
}

基准测试代码

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use matchplay_perf::{build_set, build_set_f64};

fn build_set_benchmark(c: &mut Criterion) {
    let mut group = c.benchmark_group("Build set");

    group.bench_function(
      "build_set::<f64>(min_win=25)",
      |b| b.iter(
        || build_set::<f64>(black_box(25), 0.5, 0.5, 0.5)
      )
    );

    group.bench_function(
      "build_set_f64(min_win=25)",
      |b| b.iter(
        || build_set_f64(black_box(25), 0.5, 0.5, 0.5)
      )
    );

    group.finish();
}

criterion_group!(benches, build_set_benchmark);
criterion_main!(benches);

Cargo.toml配置

[package]
name = "matchplay-perf"
version = "0.1.0"
edition = "2021"

[dev-dependencies]
criterion = "0.4"

[dependencies]
num-traits = "0.2.15"

[lib]
path = "lib/matchplay.rs"

[[bench]]
name = "benchmarks"
harness = false

问题分析与解答

关于泛型单态化的理解

你的理解没错——Rust的泛型确实会在编译时进行单态化,为每个具体类型生成独立的机器码。但性能差异的根源不在单态化本身,而在于**num_traits trait方法的调用开销**。

在泛型版本中,所有数值操作(zero()、one()、加减乘)都需要通过Num trait的方法调用实现。虽然这些调用是静态分发(编译时确定),但编译器可能无法像单态版本那样完全内联这些trait方法,或者trait的默认实现没有针对f64做足够优化。相比之下,手动单态的build_set_f64直接使用f64的原生指令,编译器可以更轻松地进行优化(比如指令重排、寄存器分配),生成更紧凑高效的机器码。

关于向量读写的疏漏

向量的读写操作本身没有问题。两种版本的向量访问逻辑完全一致,性能差异和向量操作无关。问题还是出在泛型代码中的trait方法调用,以及编译器对泛型代码的优化限制。

优化方案

  1. 为泛型函数添加#[inline(always)]:强制编译器内联泛型函数,帮助消除trait方法调用的开销。修改build_set函数的定义:

    #[inline(always)]
    pub fn build_set<T: Num+Copy>(min_win: usize, psrv: T, p1: T, p2: T) -> Set<T> {
        // ... 原有代码 ...
    }
    
  2. 使用更具体的trait约束:如果只需要处理浮点类型,可以将Num替换为num_traits::Float,该trait的方法更贴近浮点类型的原生操作,编译器优化空间更大:

    use num_traits::Float;
    
    pub fn build_set<T: Float+Copy>(min_win: usize, psrv: T, p1: T, p2: T) -> Set<T> {
        vec[0] = H2H {vs1: psrv, vs2: T::one() - psrv};
        // ... 其余代码调整为使用T的原生方法 ...
    }
    
  3. 启用更高等级的优化:确保编译时使用--release模式(criterion默认启用),也可以尝试在Cargo.toml中添加更激进的优化选项:

    [profile.release]
    opt-level = 3
    lto = true
    codegen-units = 1
    

内容的提问来源于stack exchange,提问作者dbeacham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 09:22:31