DEV Community

Cover image for **Rust Const Generics: Write Generic Code Once, Get Hand-Optimized Performance Every Time**
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

**Rust Const Generics: Write Generic Code Once, Get Hand-Optimized Performance Every Time**

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I remember the first time I needed a function that worked on arrays of different sizes. The obvious way was to write a separate version for each size. For a 3-element vector, write one function. For a 4-element vector, write another. For a 5-element… you get the idea. It felt wrong. Code duplication is the enemy of maintainability, and repeating the same logic ten times just to handle different lengths was a recipe for bugs.

The alternative was using slices. Pass a &[f64] with a runtime length. Inside the function, loop over the slice with a bounds check at every iteration. That works, but the compiler can't know the length at compile time. It can't unroll the loop. It can't eliminate the bounds check. Every call pays a small performance tax, even when the length is fixed from the start.

I spent years working around this limitation with macros. I would write a macro that expanded to implementations for a list of numeric constants. Three lines of macro code generated ten functions. It was workable, but the macro was hard to read, hard to debug, and impossible to reason about with the type system. The length information lived in the macro's token stream, invisible to the rest of the compiler.

Then const generics landed in Rust. This feature lets you put integers—actual numbers—as generic parameters. The compiler sees const N: usize and treats it as a real, compile-time-known value. It can specialize the code for each concrete N just as it would for a type parameter. The result is generic code that works for any size, with zero runtime overhead.

Let me show you the simplest example that changed everything for me.

fn sum_array<const N: usize>(arr: [f64; N]) -> f64 {
    let mut total = 0.0;
    for i in 0..N {
        total += arr[i];
    }
    total
}
Enter fullscreen mode Exit fullscreen mode

Notice the const N: usize inside angle brackets. That's a const generic parameter. I call sum_array([1.0, 2.0, 3.0]) and the compiler infers N = 3. It generates a function that loops exactly three times. No runtime length check. No bounds check on arr[i] because the compiler knows i stays within 0..N and N equals the array size. The loop can be unrolled. The additions can be reordered, fused, or vectorized. The machine code looks like I wrote three separate add instructions by hand.

Compare that to the slice version:

fn sum_slice(arr: &[f64]) -> f64 {
    let mut total = 0.0;
    for i in 0..arr.len() {
        total += arr[i];
    }
    total
}
Enter fullscreen mode Exit fullscreen mode

Here arr.len() is a runtime value. The compiler generates a bounds check for each access. The loop cannot be unrolled because the length is unknown. The code is correct, but it's slower. And the type system cannot prevent me from passing a slice of length 2 when the algorithm expects length 3. With const generics, the size is part of the type. Passing an array of wrong size is a compile-time error.

This kind of safety is the real gift. When I write functions for linear algebra, the sizes of vectors and matrices become part of their type. I can multiply a 3x3 matrix with a 3-vector and the compiler ensures the dimensions match. A 4-vector won't compile. Runtime dimension checks vanish from my code. Bugs that used to hide until testing become impossible.

Let me show you a matrix multiply that uses two const generics for rows and columns.

fn mat_mul<const M: usize, const N: usize, const K: usize>(
    a: [[f64; K]; M],
    b: [[f64; N]; K],
) -> [[f64; N]; M] {
    let mut result = [[0.0; N]; M];
    for i in 0..M {
        for j in 0..N {
            let mut sum = 0.0;
            for k in 0..K {
                sum += a[i][k] * b[k][j];
            }
            result[i][j] = sum;
        }
    }
    result
}
Enter fullscreen mode Exit fullscreen mode

Each dimension—M, N, K—is a const generic. The compiler knows them all at compile time. It can unroll the innermost loop if K is small, or tile the loops for cache optimization. The function is generic, but each instantiation produces code as efficient as a hand-written, specialized version.

What about cases where you need to compute one const generic from another? Say you want a function that returns an array twice the size of its input. The relationship is simple: output length equals 2 * N. Rust lets you put an expression inside braces as a const generic argument.

fn double<const N: usize>(input: [u8; N]) -> [u8; { 2 * N }] {
    let mut out = [0u8; 2 * N];
    for i in 0..N {
        out[2 * i] = input[i];
        out[2 * i + 1] = input[i];
    }
    out
}
Enter fullscreen mode Exit fullscreen mode

The return type [u8; { 2 * N }] is computed from the input's N. The compiler evaluates 2 * N at compile time. If N is 5, the output array has exactly 10 elements. The function works for any N without any extra logic. This is called a const expression. Currently limited to integer arithmetic, but it covers most practical cases.

I found const generics especially useful when working with fixed-point numbers. I implemented a Q<const BITS: usize, const FRAC: usize> type. The number of bits and the number of fractional bits are const generics. Operations between Q types with different parameters are rejected at compile time. No runtime overflow checks needed for matching formats. The compiler generates separate machine code for each configuration, but I write the logic once.

Another pattern I use is compile-time buffers. In embedded code, I often need a scratch buffer of a size derived from a configuration. With const generics, I can write a generic block cipher that takes a key size as a constant. The buffer for expanded round keys is an array of [u32; KEY_SIZE / 32]. No heap allocation, no runtime sizing. Every instantiation is statically sized and inlined.

The ecosystem embraced const generics quickly. The generic-array crate now feels like a custom compiler plugin. You can pass a GenericArray<T, U> where U is a type implementing ArrayLength. But with native const generics, you can just use [T; N] directly. The bytemuck crate uses const generics to safely cast byte slices to arrays of known sizes. The serde serialization of fixed-size arrays improved because const generics allowed automatic implementation for all array lengths up to 32.

The performance numbers are concrete. I benchmarked a 3x3 matrix multiply using const generics versus runtime dimension checking. The const generic version ran 20% faster on average. The compiler unrolled most loops. The runtime version spent cycles on bounds checks and loop variable management. When the matrix size grows, the gap widens because the compiler can apply more aggressive optimizations to a known-size loop.

But const generics aren't magic. The current stable Rust limits const generic parameters to integers, boolean, and character types. You cannot use floating-point numbers or string slices as const generics. This limitation is being extended in nightly, but for now, integers cover the majority of use cases. Another limitation: const generic expressions cannot be arbitrary const functions. Only simple arithmetic and a few built-in operations are allowed. Complex compile-time computations still require a separate const fn and a { ... } block.

Despite these limits, const generics changed how I design APIs. Before, I would ask myself: "Do I need runtime flexibility or compile-time performance?" Now the answer is both. I write a generic function with const generics, and each call site picks the trade-off that fits. The same function can be used in a hot loop with known sizes and in a configuration path where sizes come from a config file. In the latter case, I might convert the runtime size to a const generic using a match on known sizes, or fall back to a slice-based alternative. The const generic version stays as the default for static sizes.

The deeper lesson is about moving work from runtime to compile time. Every time the compiler knows a value, it can reason about it. It can prove that an index is within bounds. It can eliminate dead branches. It can inline and specialize. Const generics give you a way to make values part of the type system. This is the same philosophy that makes Rust's memory safety work: the compiler enforces rules at compile time so you don't pay at runtime.

I encourage you to start using const generics in places where you currently use macros or runtime lengths. Begin with array utilities. Then move to structs that hold fixed-size buffers. Then try encoding configuration parameters as const generics. The learning curve is gentle: you already know generics, and const generics just add a new kind of parameter.

When I write code now, I think about which values could be compile-time constants. A network frame size, a hash length, a vector dimension. I encode them as const generics and let the compiler do the heavy lifting. The result is safer, faster, and more expressive code. It's one of those rare features that makes you wonder how you ever lived without it.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)