How to Do Linear Algebra in Rust (nalgebra, faer)

TITLE: How to Do Linear Algebra in Rust (nalgebra, faer)

When matrices need rules

You're building a physics simulation. You have a vector of positions and a matrix of forces. You multiply them, and the result is garbage. Or worse, the code runs, but it takes three seconds to process what Python did in three milliseconds. Linear algebra in Rust feels different. The compiler checks your dimensions at compile time. The performance is on par with C++. But the ecosystem splits into two distinct camps, and picking the wrong one can cost you weeks of refactoring.

You might reach for a crate and find that nalgebra refuses to let you multiply a 2x3 matrix by a 2x3 matrix. The compiler rejects you before the code runs. Meanwhile, faer asks you to configure a backend and assumes you know exactly how large your data will be. Both crates are excellent. They solve different problems.

Two camps, two goals

Rust's linear algebra landscape divides by philosophy. nalgebra treats matrices as first-class citizens with strict geometric rules. It knows the size of your matrix at compile time. It distinguishes between points, vectors, and matrices. It includes quaternions, isometries, and rotation types. It's the choice for game engines, robotics, and any code where geometry matters.

faer takes a different path. It prioritizes raw throughput above all else. It uses dynamic dimensions by default and leans on optimized backends like BLAS or GPU kernels. It's built for heavy lifting where the matrix size changes at runtime and you need to squeeze every cycle out of the hardware.

Think of nalgebra as a drafting table with a ruler built into the paper. You can't draw a line that violates the grid. Think of faer as a supercomputer that assumes you know what you're doing and optimizes the hell out of it, but you have to feed it data in a specific way.

Static dimensions with nalgebra

nalgebra shines when your matrix sizes are fixed. The crate uses the type system to encode dimensions. A 2x2 matrix has a different type than a 3x3 matrix. This prevents entire classes of bugs.

use nalgebra::{Matrix2, SMatrix};

/// Demonstrates compile-time dimension safety.
fn main() {
    // SMatrix encodes dimensions in the type signature.
    // Matrix2 is an alias for SMatrix<f64, 2, 2>.
    let rotation = Matrix2::new(0.0, -1.0, 1.0, 0.0);

    // The compiler checks dimensions here.
    // 2x2 times 2x2 produces a 2x2.
    let squared = rotation * rotation;

    println!("Rotation squared: {}", squared);
}

When you write Matrix2::new, you're creating a stack-allocated struct. The 2 and 2 aren't just numbers; they're part of the type. This means rotation has the type SMatrix<f64, 2, 2>. When you multiply, the compiler sees SMatrix<f64, 2, 2> * SMatrix<f64, 2, 2> and knows the result is SMatrix<f64, 2, 2>. It generates unrolled loops. No function calls. No bounds checks. The math happens as fast as the CPU can crunch it.

Static sizes mean zero-cost abstractions.

Dynamic data and realistic workflows

Real code rarely stays fixed size. You might load a transformation matrix from a file, or process a batch of images where the dimensions vary. nalgebra handles this with DMatrix and DVector. These types allocate on the heap and determine size at runtime.

use nalgebra::{DMatrix, DVector};

/// Processes a dynamic matrix and handles singular cases.
fn process_data() {
    // DMatrix allocates on the heap.
    // Dimensions are known only at runtime.
    let data = DMatrix::from_row_slice(3, 3, &[
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0,
        7.0, 8.0, 9.0,
    ]);

    // Inverse requires the matrix to be square and non-singular.
    // This returns an Option because the inverse might not exist.
    if let Some(inv) = data.try_inverse() {
        println!("Inverse exists: {}", inv);
    } else {
        println!("Matrix is singular. No inverse.");
    }
}

DMatrix pays a price for flexibility. It stores dimensions in the value, not the type. The compiler can't check multiplication dimensions at compile time. If you multiply a 3x3 by a 2x2, the code compiles, but you get a panic at runtime. The crate checks dimensions when you call the operation and panics if they don't match.

Singular matrices don't panic; they return None. Handle the error.

Geometric types and conventions

nalgebra goes beyond raw matrices. It defines Point and Vector as distinct types. A vector represents direction and magnitude. A point represents a location in space. You can add a vector to a point to get a new point. You can subtract two points to get a vector. You cannot add two points. The compiler enforces this.

This distinction prevents bugs where you accidentally add two coordinates together. In many languages, points and vectors are the same struct. In nalgebra, the type system stops you from making geometric nonsense.

Convention matters here. Use SVector for fixed-size vectors. Use DVector for dynamic vectors. Use Point for locations. Don't use matrices to represent points. The crate provides the right types for the job.

Another convention: nalgebra uses feature flags for optional functionality. If you need serialization, add serde to your Cargo.toml. If you need random number generation, add rand. The core crate stays lightweight.

[dependencies]
nalgebra = { version = "0.33", features = ["serde", "rand"] }

The community expects you to enable only the features you need. Keep your dependency graph clean.

High-performance with faer

When your matrices grow large, nalgebra's generic approach can hit limits. faer steps in. It's designed for numerical computing at scale. It supports BLAS backends, GPU offloading, and aggressive optimizations.

use faer::cpu::Cpu;
use faer::Matrix;

/// Sets up faer with a CPU backend.
fn main() {
    // faer requires a backend configuration.
    // Cpu::new() sets up the CPU backend with default settings.
    let cpu = Cpu::new();

    // Create a matrix from a closure.
    // The closure defines the value at each index.
    let mat = Matrix::from_fn(&cpu, 4, 4, |i, j| {
        if i == j { 1.0 } else { 0.0 }
    });

    // faer operations often return a handle or require explicit execution.
    // This depends on the specific API version.
    println!("Identity matrix via faer: {}", mat);
}

faer doesn't just work out of the box. You need to configure a backend. The Cpu backend uses optimized kernels. You can swap in blis or openblas via features for even more speed. If you forget to configure the backend, you might get a panic or fall back to a slow path.

faer also handles dynamic dimensions efficiently. It's built for heap-allocated data. It doesn't try to encode sizes in the type system. It focuses on throughput.

faer demands a backend. Configure it or pay the penalty.

Pitfalls and compiler errors

Linear algebra in Rust trips up developers in predictable ways.

If you try to multiply a Matrix3 by a Matrix2, you get E0308 (mismatched types). The compiler sees the dimensions in the type and refuses to compile. This is a feature. It catches bugs early.

If you use DMatrix and multiply incompatible sizes, the code compiles. You get a panic at runtime. The crate checks dimensions and panics if they don't match. Always validate dimensions when working with dynamic data.

If you try to invert a singular matrix, nalgebra returns None. Don't unwrap blindly. Check the result. A singular matrix has no inverse. Unwrapping causes a panic.

If you mix nalgebra and faer, you might hit type mismatches. The crates don't share types. You'll need to convert data between them. nalgebra provides conversion traits, but they can be verbose. Plan your architecture carefully.

If you forget nalgebra features, you might get E0277 (trait bound not satisfied). For example, if you try to serialize a matrix without the serde feature, the compiler complains. Enable the features you need.

Decision matrix

Use nalgebra when you need compile-time dimension safety. The type system catches multiplication errors before you run the code. Use nalgebra when you're working with small, fixed-size matrices like 2D transforms or 3D rotations. The compiler unrolls loops and eliminates overhead. Use nalgebra when you need geometric types. It includes points, vectors, quaternions, and isometries that understand how to interact with each other.

Use faer when you're crunching large matrices and need raw throughput. It integrates with BLAS backends and supports GPU offloading. Use faer when your matrix sizes are dynamic and determined at runtime. It handles heap-allocated data efficiently. Use faer when you're building a high-performance numerical backend. It's designed for libraries that demand maximum speed.

nalgebra for geometry and safety. faer for scale and speed.

Where to go next