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

小型f32数组性能异常:长度为2时性能骤降问题排查

问题:f32二维向量const generics实现的性能异常

在N体模拟项目中,使用const generics封装Array<const DIM: usize>表示f32代数向量,测试时发现2维数组的性能显著落后于1、3、4、5维等其他规格——即使去掉封装直接用原生数组,结果依然一致。换成f64类型则无此性能差异。

核心场景简化代码:

fn fold<const N: usize>(v: Vec<Array<N>>) -> Vec<Array<N>> {
    let result = v.iter().map(|a1| {
        v.iter().fold(Array::default(), |acc, a2| {
            let d = *a2 - *a1;
            acc + d
        })
    });
    result.collect()
}

完整基准测试代码:

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use rand::{thread_rng, Rng};

#[derive(Clone, Copy, Debug)]
pub struct Array<const DIM: usize>(pub [f32; DIM]);

impl<const DIM: usize> Default for Array<DIM> {
    fn default() -> Self {
        Self([0.0; DIM])
    }
}

impl<const DIM: usize> std::ops::Add for Array<DIM> {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        let mut result = Array::default();
        for (i, val) in result.0.iter_mut().enumerate() {
            *val = self.0[i] + rhs.0[i];
        }
        result
    }
}

impl<const DIM: usize> std::ops::Sub for Array<DIM> {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        let mut result = Array::default();
        for (i, val) in result.0.iter_mut().enumerate() {
            *val = self.0[i] - rhs.0[i];
        }
        result
    }
}

fn random_arrays<const N: usize>(size: usize) -> Vec<Array<N>> {
    let mut rng = thread_rng();
    let mut gen = || rng.gen_range(-100.0..100.0);
    let mut v = Vec::new();
    for _ in 0..size {
        v.push(Array([0.0; N].map(|_| gen())));
    }
    v
}

fn fold<const N: usize>(v: Vec<Array<N>>) -> Vec<Array<N>> {
    let result = v.iter().map(|a1| {
        v.iter().fold(Array::default(), |acc, a2| {
            let d = *a2 - *a1;

            acc + d
        })
    });

    result.collect()
}

fn criterion_benchmark(c: &mut Criterion) {
    let mut group = c.benchmark_group("Array fold");

    for i in (5000..=5000).step_by(10) {
        group.bench_with_input(BenchmarkId::new("Length 1", i), &i, |b, i| {
            b.iter(|| fold::<1>(black_box(random_arrays(*i))))
        });
        group.bench_with_input(BenchmarkId::new("Length 2", i), &i, |b, i| {
            b.iter(|| fold::<2>(black_box(random_arrays(*i))))
        });
        group.bench_with_input(BenchmarkId::new("Length 3", i), &i, |b, i| {
            b.iter(|| fold::<3>(black_box(random_arrays(*i))))
        });
        group.bench_with_input(BenchmarkId::new("Length 4", i), &i, |b, i| {
            b.iter(|| fold::<4>(black_box(random_arrays(*i))))
        });
        group.bench_with_input(BenchmarkId::new("Length 5", i), &i, |b, i| {
            b.iter(|| fold::<5>(black_box(random_arrays(*i))))
        });
    }

    group.finish();
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
性能差异原因解析
  • SIMD指令集利用效率差异:
    x86架构下,f32单元素占4字节,2维数组总大小8字节,仅为64位SIMD寄存器(如xmm)的一半。编译器对这种“半满”寄存器的数组优化策略保守:1维可直接打包进寄存器,3维可拆分到多个寄存器或用AVX256位寄存器,4维刚好填满xmm寄存器,这些场景都会生成高效的SIMD指令;而2维数组往往需要额外的打包/拆包操作,或退化为标量运算,导致指令开销翻倍。换成f64时,2维数组总大小16字节,刚好填满xmm寄存器,因此无性能差距。

  • LLVM后端的特化优化偏向:
    Rust的const泛型数组在LLVM编译阶段会根据长度做特化处理。对于2维f32数组,LLVM的循环展开、寄存器分配策略不如其他长度高效——比如其他长度的加减循环会被完全展开并映射到SIMD指令,而2维可能保留冗余的循环结构,或生成低效的寄存器操作序列。

  • 内存缓存的次要影响:
    2维f32数组的8字节大小在缓存行中的填充方式,相比12、16字节的数组,可能导致缓存利用率略低,但这不是核心因素,性能差异的主要来源还是SIMD指令的利用效率。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 19:10:51