ndarray



presented by Ulrik Sverdrup (bluss)

22 March 2016

https://github.com/bluss/rust-ndarray

What is ndarray?

What is ndarray?

A multidimensional container for general elements and for numerics

It’s an array with multiple dimensions.

use ndarray::OwnedArray;

let mut array = OwnedArray::zeros((3, 5, 5));
array[[1, 1, 1]] = 9.;

What is ndarray?

It is bounds checked like a regular Rust data structure:

array[[3, 1, 1]] = 10.;
// PANIC! thread '<main>' panicked at 'ndarray: index [3, 1, 1] is out of bounds for array of shape [3, 5, 5]'

and it supports numerics:

let x = OwnedArray::from_vec(vec![0., 1., 2., 1.]);
let x_hat = &x / x.scalar_sum();
println!("{:5.2}", x_hat);
// OUTPUT: [ 0.00,  0.25,  0.50,  0.25]

Array Types

Representation

Representation

Representation

Representation

Slicing

Splitting

Views and Iterators

.inner_iter()     // each iterator has a corresponding
.outer_iter()     // mutable version too.
.axis_iter(Axis)
.axis_chunks_iter(Axis, usize)

Iterators are a powerful way to access views of an array.

An Array View of Anything

// Create a stack allocated Hilbert Matrix 
let mut data = [0.; 1024];
let mut view = ArrayViewMut::from(&mut data[..])
                            .into_shape((32, 32)).unwrap();
for ((i, j), elt) in view.indexed_iter_mut() {
    *elt = 1. / (1. + i as f32 + j as f32);
}

Higher Order Functions

// Closure types in pseudocode!
.map(&self, |&A| -> B) -> OwnedArray<B, D>
.mapv(&self, |A| -> B) -> OwnedArray<B, D>
.map_inplace(&mut self, |&mut A|)
.mapv_inplace(&mut self, |A| -> A)

.zip_mut_with(&mut self, rhs: &Array<B>, |&mut A, &B|)

Higher order functions are a powerful way to traverse / modify an array element by element. They give ndarray flexibility to perform the operation as efficiently as possible.

Performance

These operations are efficient in ndarray. Our Rust code is autovectorized by the compiler.

/* Unary op */  array1 += 1.;
/* Unary op */  array1.mapv_inplace(f32::abs);
/* Binary op */ array1 += &array2;
/* Reduction */ array1.scalar_sum();

(Overloading += is a Rust 1.8 feature
— stable soon! )

Matrix multiplication and linear algebra is another story, uses integration with BLAS.

Performance

These operations are efficient in ndarray. Our Rust code is autovectorized by the compiler.

/* Unary op */  array1 += 1.;
/* Unary op */  array1.mapv_inplace(f32::abs);
/* Binary op */ array1 += &array2;
/* Reduction */ array1.scalar_sum();

(Overloading += is a Rust 1.8 feature
— stable in )

Matrix multiplication and linear algebra is another story, uses integration with BLAS.

Performance Secret 1

Performance Secret 1

Performance Secret 1

Ndarray operations are efficient when they access the underlying data as a slice.

fn unary_operation(data: &mut [f32]) {
    for element in data {
        *element += 1.;
    }
}

Performance Secret 2

Iterate two slices in lock step.

fn binary_operation(a: &mut [f32], b: &[f32]) {
    let len = std::cmp::min(a.len(), b.len());
    let a = &mut a[..len];
    let b = &b[..len];

    for i in 0..len {
        a[i] += b[i];
    }
}

Performance Secret 3

Autovectorize a floating point sum.

Ideally...

fn sum(data: &[f32]) -> f32 {
    let mut sum = 0.;
    for &element in data {
        sum += element;
    }
    sum
}

Performance Secret 3

Autovectorize a floating point sum.

fn sum(mut data: &[f32]) -> f32 {
    let (mut s0, mut s1, mut s2, mut s3,
         mut s4, mut s5, mut s6, mut s7) =
        (0., 0., 0., 0., 0., 0., 0., 0.);

    while data.len() >= 8 {
        s0 += data[0]; s1 += data[1];
        s2 += data[2]; s3 += data[3];
        s4 += data[4]; s5 += data[5];
        s6 += data[6]; s7 += data[7];
        data = &data[8..];
    }
    let mut sum = 0.;
    sum += s0 + s4; sum += s1 + s5;
    sum += s2 + s6; sum += s3 + s7;
    for i in 0..data.len() {
        sum += data[i];
    }
    sum
}

Performance Secrets

Ndarray operations are efficient when they access the underlying data as a slice (they do when the memory layout allows).

Design Choices

End