nd-range

Vector Math - Standard Library

Motivation

The Rust standard library provides several 'Range' types which represent integers inside a given bounds (i.e. 1 ≤ n ≤ 100). Ranges can be iterated over, and used for bounds testing of numbers. In designing video games, it is often necessary to express 2 or 3 dimensional quantities or bounds. Problems like iterating over voxels in a 3D world, or testing for an intersection between rectangles lend themselves to the idea of Ranges.

// A 3D array representing a 128x128x128 voxel world
let world_data = [[[0; 128]; 128]; 128];
// The '0..128' here is a range from 0 to 127 inclusive
for x in 0..128 {
  for y in 0..128 {
    for z in 0..128 {
      let voxel = &world[x][y][z];
      // Do something with voxel
    }
  }
}
However, in these contexts the Range type is not particularly helpful. I am forced to use three different ranges and a triple nested 'for' loop. The idea for nd-range is to expand the native Range type so that it can be used over arbitrary dimensions. This allows us to iterate over X, Y, and Z coordinates in a single loop.

let world_data = [[[0; 128]; 128]; 128];
for [x, y, z] in nrange!(0..128, 0..128, 0..128) {
  let voxel = &world[x][y][z];
  // Do something with voxel
}

Approach

An nd-range is similar in concept to an axis-aligned bounding box, or AABB. These are common abstractions used in game development, and there are well established algorithms that test for overlap. Iterating over is slightly more complex, requiring some vector algebra. If we interpret the range of values over each axis as a vector, we can apply the cartesian product algorithm to find every position within the bounded space. Below is a diagram from Wikipedia (CC BY-SA 3.0). Because a range is contiguous by definition, generating a cartesian product is simple and performant. The iterator object has a space complexity of O(N), where n is the number of dimensions. Rust's const generics allow the entire struct to exist on the stack, which is a boon to performance.

Performance

My points of comparison are the itertools and cartesian crates, which provide comparable algorithms. Despite its popularity, I was shocked to discover how slow the itertools implementation of the cartesian product is. The cartesian-rs crate is lesser known, and uses a creative macro-based solution. I benchmarked all three approaches, as well as the nested for-loop base case, over a 100x100x100 range.
Implementation Average Time (ns) Error (+/- ns)
itertools 1,821,573 31,501
cartesian-rs 989,242 50,835
nd-range 968,853 15,792
nested loops 911,853 46,066

Pros and Cons

My implementation is competitive with, and possibly faster than its competitors for my use case. While nd-range only works for ranges of contiguous integers, itertools and cartesian-rs work for any iterators. However, by restricting my use case I can extract more performance gains and integrate better with the Rust standard library.