tandem rng tandem-rs GitHub

Design

Fills

The eight lane states of a row stay in registers as u32x4 vectors, the row store is a 4x4 transpose by interleaves, and every integer fill writes the same byte stream, so one routine serves all widths. By default floats convert in a second pass over the row while it is in L1. The seed rounds run on a local that swaps its halves by value, since mem::swap of the fields made LLVM split the vectors into 64-bit halves.

simd-intrinsics maps each 16-byte block to floats before the store: ucvtf with 24 or 53 fraction bits on AArch64, the signed convert for f32 and an exact two-part exponent trick for f64 on SSE2, which has no 64-bit convert. It also forms both halves of the 32x32 to 64-bit products from two widening multiplies (umull and umull2 with two unzips, or two pmuludq). On AArch64 LLVM already uses umull for the high half, so integers do not gain there. On SSE2 the portable multiply costs four pmuludq per product.

The 32- and 64-bit draws inline into the caller. A draw at an aligned position in the readable row is a load and an add, and the one call is the row refill, after which the draw stores the position, so a loop keeps the position in a register. The refill makes its row readable and steps the cache one row ahead into the other row slot. The next refill then finds its row computed, and no draw reads words just written by vector stores, which store forwarding serves badly. The slots hold the exposed words in stream order, the order the draws read.

The struct is repr(C): with the default layout the compiler pairs the loads of the position and the readable row's position into one 16-byte load right after the 8-byte store of the position, which defeats store forwarding and doubles the cost of a scalar draw.

Bounded integers

Normals

Weighted choice

Exponentials