Libmir Archive

ndslice — Multidimensional Arrays

mir.ndslice is the core data structure in mir-algorithm. It provides N-dimensional array slices with contiguous or strided memory, lazy views, and full BetterC support.

mir.ndslice is mir-algorithm's answer to NumPy's ndarray. A Slice holds a pointer, a shape (array of lengths per dimension), and strides — giving you zero-copy views, reshapes, and transposes without ever copying data.

Old URL: This page was previously at mir-algorithm.libmir.org/mir_ndslice.html. That URL now redirects here.

The Slice type

struct Slice(Iterator, size_t N = 1, SliceKind kind = Contiguous) { ... }
  • Iterator — usually a pointer (T*) for contiguous memory, or a custom iterator
  • N — number of dimensions (compile-time constant)
  • kindContiguous, Canonical, or Universal (with full strides)

In practice you rarely name the full type; use auto or the helper aliases.

Creating slices

Allocate new memory

import mir.ndslice;

auto v = slice!double(5);         // 1-D, length 5
auto m = slice!int(3, 4);         // 2-D, 3 rows × 4 cols
auto t = slice!float(2, 3, 4);    // 3-D tensor

slice!T(...) allocates GC memory. For @nogc code use rcslice (reference-counted) or wrap raw memory with sliced.

Wrap existing memory

double[12] buf;
auto m = buf[].sliced(3, 4);   // no allocation, owns nothing

From an array literal

auto v = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0].sliced(2, 3);
// [[1, 2, 3],
//  [4, 5, 6]]

iota — index sequences

import mir.ndslice.topology : iota;

auto idx = iota(3, 4);
// [[0,  1,  2,  3],
//  [4,  5,  6,  7],
//  [8,  9, 10, 11]]

iota produces a lazy slice — no memory allocated.

Indexing and slicing

auto m = iota(4, 5);

m[2, 3]          // scalar element at row=2, col=3 → 13
m[1 .. 3, 1 .. 4]  // sub-slice rows 1–2, cols 1–3 (view, no copy)
m[0]             // first row as a 1-D slice
m[$ - 1]         // last row

Shape, length, strides

auto m = slice!double(3, 4);

m.shape          // [3, 4]  (static array of lengths)
m.length         // 3       (length of first dimension)
m.length!1       // 4       (length of second dimension)
m.elementCount   // 12
m.strides        // [4, 1]  in a contiguous slice

Assignment and filling

auto m = slice!double(3, 4);

m[] = 0.0;                     // fill all elements
m[] = iota!double(3, 4);       // copy from iota
m[0] = [1.0, 2.0, 3.0, 4.0];  // assign first row

// Element-wise arithmetic (via topology.map):
import mir.ndslice.topology : map;
auto doubled = m.map!(x => x * 2);  // lazy, no copy

Iteration

import mir.ndslice.topology : flattened;
import mir.algorithm.iteration : each, reduce;

// Iterate all elements
m.each!(x => writeln(x));

// Flatten to 1-D for reduction
double sum = m.flattened.reduce!"a + b"(0.0);

// Index-aware iteration
m.each!((ref double v, size_t i, size_t j) {
    v = cast(double)(i + j);
});

Memory kinds

KindDescriptionUse case
ContiguousSingle pointer, no strides storedFreshly allocated slices
CanonicalLast dimension stride is 1, others storedSlices of rows
UniversalAll strides storedTransposed views, arbitrary strides

Most operations accept all three kinds. Algorithms in mir.algorithm work on all kinds.

Reference-counted slices (rcslice)

For @nogc and -betterC code without the GC:

import mir.rc.array : rcslice;

auto m = rcslice!double(3, 4);   // reference-counted, no GC
// Freed automatically when last reference goes out of scope

Comparison with std.array / built-in arrays

// D built-in 2-D array (jagged)
double[][] mat = new double[][](3, 4);   // 3 separate heap allocations
mat[1][2] = 5.0;

// ndslice (contiguous)
auto mat2 = slice!double(3, 4);          // 1 heap allocation
mat2[1, 2] = 5.0;
// mat2.ptr is a single double* — directly usable with BLAS

On this page