如何使用 ndarray?
解读
在国内 Rust 后端/算法岗面试中,**“ndarray 怎么用”**并不是让你背 API,而是考察三点:
- 是否理解 Rust 没有 GC 的内存模型,能否用 ndarray 写出零拷贝、无别名可变借用的代码;
- 是否掌握维度变换、轴切片、广播、并行迭代四大高频场景;
- 能否把 ndarray 与 SIMD、rayon、BLAS 生态打通,给出性能可落地的方案。
一句话:面试官想听你“安全且高性能地操纵 n 维矩阵”,而不是“能跑就行”。
知识点
- ArrayBase 的三泛型参数
ArrayBase<S, D>:存储类型S(Owned/View/MutView)与维度类型D(Ix1/Ix2/Dyn)决定能否编译期定长优化。 - 所有权与视图分离:
array.view()返回只读视图,array.view_mut()返回可写视图,二者与原始数组共享同一块数据,但 Rust 借用检查保证同时只能存在一个可变视图。 - 轴(axis)与形状(shape):
array.shape()返回[usize],array.axis_iter(axis)产生不跨轴的连续子视图,对行/列做并行计算时优先用axis_par_iter。 - 广播规则:维度从尾向前对齐,短维度为 1 可扩展,与 NumPy 一致,但 Rust 在编译期不检查,运行期
broadcast返回Option,必须用if let Some(b) = a.broadcast(...)显式处理失败。 - BLAS 后端切换:
ndarray-linalg与blas-srcfeature 可一键换 OpenBLAS/Intel MKL,在 Cargo.toml 中开启features = ["openblas-system"]即可链接系统库,避免自己编 OpenBLAS 踩坑。 - 并行加速:
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。
拓展思考
- 动态维度 vs 静态维度:如果维度在编译期已知(如 3×3 旋转矩阵),改用
Array<f32, Ix2>比ArrayD<f32>性能高 20% 以上,因为循环展开和 SIMD 向量化更友好。 - 避免临时分配:链式调用
mapv会产生中间数组,用azip!宏(ndarray 提供)一次性遍历多个数组,可消除 90% 临时内存。 - no_std 嵌入式:在 Cortex-M 上关闭
stdfeature,用ArrayViewMut直接操作 DMA 缓冲区,实现零拷贝信号处理,但需手动保证对齐到 32 字节以触发 SIMD。 - 与 tokio 异步互操作:ndarray 本身不做 IO,若要在 async 上下文做流式矩阵乘,可把大块计算 spawn 到
tokio::task::spawn_blocking,避免阻塞调度线程。 - 面试反杀问题:当面试官问“ndarray 和 nalgebra 怎么选”时,回答“看维度是否编译期固定:固定用 nalgebra,动态用 ndarray”,并补充“nalgebra 支持 const 泛型维度,能生成完全无运行时检查的代码”,可瞬间加分。