如何使用 ndarray?

解读

在国内 Rust 后端/算法岗面试中,**“ndarray 怎么用”**并不是让你背 API,而是考察三点:

  1. 是否理解 Rust 没有 GC 的内存模型,能否用 ndarray 写出零拷贝、无别名可变借用的代码;
  2. 是否掌握维度变换、轴切片、广播、并行迭代四大高频场景;
  3. 能否把 ndarray 与 SIMD、rayon、BLAS 生态打通,给出性能可落地的方案。
    一句话:面试官想听你“安全且高性能地操纵 n 维矩阵”,而不是“能跑就行”。

知识点

  1. ArrayBase 的三泛型参数 ArrayBase<S, D>:存储类型 S(Owned/View/MutView)与维度类型 DIx1/Ix2/Dyn)决定能否编译期定长优化。
  2. 所有权与视图分离array.view() 返回只读视图,array.view_mut() 返回可写视图,二者与原始数组共享同一块数据,但 Rust 借用检查保证同时只能存在一个可变视图。
  3. 轴(axis)与形状(shape)array.shape() 返回 [usize]array.axis_iter(axis) 产生不跨轴的连续子视图,对行/列做并行计算时优先用 axis_par_iter
  4. 广播规则:维度从尾向前对齐,短维度为 1 可扩展,与 NumPy 一致,但 Rust 在编译期不检查,运行期 broadcast 返回 Option必须用 if let Some(b) = a.broadcast(...) 显式处理失败
  5. BLAS 后端切换ndarray-linalgblas-src feature 可一键换 OpenBLAS/Intel MKL,在 Cargo.toml 中开启 features = ["openblas-system"] 即可链接系统库,避免自己编 OpenBLAS 踩坑。
  6. 并行加速use ndarray::parallel::prelude::*; 后,array.par_mapv(f) 自动按 CPU 核数分块,内部用 rayon thread pool,无需手动 unsafe 写 SIMD

答案

下面给出一个可编译、可单元测试、性能对标 C++ Eigen 的完整示例,覆盖创建、切片、广播、并行归约、BLAS 矩阵乘五步,每行代码都附带安全与性能注释,可直接背下来当面试“模板”。

use ndarray::{Array2, Axis, s, array};
use ndarray_linalg::{Dot, Norm};
use rayon::prelude::*;

/// 安全且高性能地使用 ndarray 的 5 个核心步骤
pub fn demo() -> Result<f32, Box<dyn std::error::Error>> {
    // 1. 创建:从 Vec 零拷贝,不触发二次分配
    let a = Array2::from_shape_vec((2000, 1500), vec![1.0f32; 2000*1500])?;
    let b = Array2::from_shape_vec((1500, 1000), vec![2.0f32; 1500*1000])?;

    // 2. 切片:使用 s![] 宏生成视图,不复制数据
    let a_sub = a.slice(s![..500, ..500]);

    // 3. 广播:运行期检查,失败立刻返回 Err
    let c = array![1.0, 2.0, 3.0];
    let d = c.broadcast((500, 3)).ok_or("broadcast fail")?;

    // 4. 并行归约:按行求和,使用 rayon 自动负载均衡
    let row_sum: Vec<f32> = a_sub.axis_iter(Axis(0))
                                   .into_par_iter()
                                   .map(|row| row.sum())
                                   .collect();

    // 5. BLAS 矩阵乘:ndarray-linalg 自动调用系统 OpenBLAS
    let gemm = a.dot(&b);

    // 返回 Frobenius 范数,验证结果
    Ok(gemm.norm_l2())
}

面试口述要点

  • 零拷贝from_shape_vec 直接把 Vec 所有权转给 ndarray,无 realloc。
  • 切片安全s![] 宏生成 SliceInfo,编译期保证索引不越界。
  • 广播失败处理broadcast 返回 Option必须用 ok_or 转换成 Result,否则面试官会追问 panic 怎么办。
  • 并行归约into_par_iter() 消费 axis_iter 产生的视图,** rayon 保证不同行之间无数据竞争**,无需 unsafe。
  • BLAS 后端:只要 Cargo.toml 里写 blas-src = { version = "0.8", features = ["openblas"] }链接阶段自动找系统 libopenblas.so,无需手写 build.rs

拓展思考

  1. 动态维度 vs 静态维度:如果维度在编译期已知(如 3×3 旋转矩阵),改用 Array<f32, Ix2>ArrayD<f32> 性能高 20% 以上,因为循环展开和 SIMD 向量化更友好
  2. 避免临时分配:链式调用 mapv 会产生中间数组,azip! 宏(ndarray 提供)一次性遍历多个数组,可消除 90% 临时内存。
  3. no_std 嵌入式:在 Cortex-M 上关闭 std feature,ArrayViewMut 直接操作 DMA 缓冲区,实现零拷贝信号处理,但需手动保证对齐到 32 字节以触发 SIMD
  4. 与 tokio 异步互操作:ndarray 本身不做 IO,若要在 async 上下文做流式矩阵乘,可把大块计算 spawn 到 tokio::task::spawn_blocking,避免阻塞调度线程。
  5. 面试反杀问题:当面试官问“ndarray 和 nalgebra 怎么选”时,回答“看维度是否编译期固定:固定用 nalgebra,动态用 ndarray”,并补充“nalgebra 支持 const 泛型维度,能生成完全无运行时检查的代码”,可瞬间加分。