Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/developer-guide/benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@ benchmark binary takes. CodSpeed reports exactly that: its performance report on
request lists the per-iteration time under `HEAD` for every benchmark the pull request adds
or changes, so check any new benchmark there before merging.

### Keep per-iteration work above the harness floor

The budget has a floor as well as a ceiling. CodSpeed runs each benchmark once and adds a fixed
cost of roughly half a microsecond of reported time around the closure. A closure that does tens
of nanoseconds of real work, such as one small allocation or a fast path that finds nothing to
do, reports mostly that floor, and the floor moves by more than 10% between runs of identical
code. Such benchmarks flag regressions on pull requests that do not touch Rust at all.

Aim for at least a few microseconds of real work per iteration:

- When the operation itself is tiny, repeat it a fixed number of times inside the closure and
black-box each result, as `vortex-buffer/benches/allocation.rs` does.
- Drop degenerate inputs, such as a zero-byte allocation or a compaction with nothing to move.
- Size inputs by bytes rather than element count so narrow and wide types land in the same
range, as `vortex-array/benches/filter_fixed_width.rs` does.

Benchmarks tagged `#[cpu_features]` run on the walltime legs instead, where the floor is timer
resolution and per-iteration jitter. Give those at least tens of microseconds per iteration, and
keep the working set inside the L2 cache of the leg machines (1 MiB on the Graviton leg) when the
benchmark is about kernel code rather than memory bandwidth.

### Gate CodSpeed-incompatible benchmarks

Use `#[cfg(not(codspeed))]` for benchmarks that are incompatible with CodSpeed.
Expand Down
4 changes: 3 additions & 1 deletion encodings/runend/benches/run_end_compress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
session
});

// (length, run_step). The (4_000, 1024) point is left out: it flipped by 20% between runs of
// identical code on more than 40 pull requests, and (10_000, 1024) and (10_000, 4096) keep long
// runs covered.
const BENCH_ARGS: &[(usize, usize)] = &[
(1000, 4),
(1000, 16),
(1000, 256),
(4_000, 4),
(4_000, 16),
(4_000, 256),
(4_000, 1024),
(10_000, 4),
(10_000, 16),
(10_000, 256),
Expand Down
18 changes: 10 additions & 8 deletions vortex-array/benches/binary_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,23 @@ static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
const LEN: usize = 4_096;

/// Primitive cases process at least this many rows and this many value bytes per varying input.
/// This lengthens narrow integer cases while keeping the current CodSpeed simulations below 1 ms.
///
/// The primitive cases run on the walltime legs, where short iterations are noisy: at 96 KiB per
/// input the `u32` multiply took under 6 µs and flipped by 12% between runs of identical code.
/// 256 KiB per input makes every iteration several times longer while two inputs and the output
/// still fit in a 1 MiB L2 cache.
const MIN_PRIMITIVE_LEN: usize = 16_384;
const MIN_PRIMITIVE_INPUT_BYTES: usize = 96 * 1_024;
const MIN_PRIMITIVE_INPUT_BYTES: usize = 256 * 1_024;

const I8_LEN: usize = primitive_len::<i8>();
const I16_LEN: usize = primitive_len::<i16>();
const I32_LEN: usize = primitive_len::<i32>();
const I64_LEN: usize = primitive_len::<i64>();

/// Per-row against per-row, short and long. This is the shape the operators are tuned for, so it
/// is the one every operator is measured on.
const BINARY_SHAPE_CASES: &[(usize, BinaryShape)] = &[
(128, BinaryShape::PerRowPerRow),
(I64_LEN, BinaryShape::PerRowPerRow),
];
/// Per-row against per-row at the long length. This is the shape the operators are tuned for, so
/// it is the one every operator is measured on. A 128-row case used to sit alongside it, but at
/// about 2 µs per iteration its walltime moved by up to 95% between runs of identical code.
const BINARY_SHAPE_CASES: &[(usize, BinaryShape)] = &[(I64_LEN, BinaryShape::PerRowPerRow)];

/// Constant operands are measured on `Add` alone, at the long length.
///
Expand Down
58 changes: 37 additions & 21 deletions vortex-array/benches/filter_fixed_width.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,19 @@ fn main() {

static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);

// Keep each case small: the sweep has 24 cases and the targeted sections add only seven more.
// Sized to keep CodSpeed simulation under 1ms per benchmark.
const LEN: usize = 4_096;
/// Bytes of values per input array.
///
/// Inputs are sized by bytes rather than element count. A fixed element count left the narrow
/// widths far too small: a 4096-element `i8` filter runs in about a microsecond, so CodSpeed
/// reported mostly its fixed per-benchmark cost and the number moved by up to 45% between runs
/// of identical code. 64 KiB keeps every width above that floor, and the widest types, which
/// dominate the simulation time, stay well under the 1 ms budget.
const INPUT_BYTES: usize = 64 * 1024;

/// Element count of a `T`-wide input.
const fn len_of<T>() -> usize {
INPUT_BYTES / size_of::<T>()
}
const DENSITIES: &[f64] = &[0.01, 0.5, 0.8, 0.95];
const CACHED_DENSITIES: &[f64] = &[0.01, 0.1];

Expand All @@ -54,34 +64,35 @@ enum Pattern {

const PATTERNS: &[Pattern] = &[Pattern::Random, Pattern::Runs, Pattern::Contiguous];

fn random_mask(density: f64) -> Mask {
fn random_mask(len: usize, density: f64) -> Mask {
let threshold = (density * u64::MAX as f64) as u64;
let mut state = 0x1234_5678_9abc_def0u64;
Mask::from_buffer(BitBuffer::from_iter((0..LEN).map(|_| {
Mask::from_buffer(BitBuffer::from_iter((0..len).map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state <= threshold
})))
}

fn pattern_mask(pattern: Pattern) -> Mask {
fn pattern_mask(len: usize, pattern: Pattern) -> Mask {
match pattern {
Pattern::Random => random_mask(0.5),
Pattern::Runs => Mask::from_iter((0..LEN).map(|index| (index / 32).is_multiple_of(2))),
Pattern::Contiguous => Mask::from_slices(LEN, vec![(LEN / 4, LEN * 3 / 4)]),
Pattern::Random => random_mask(len, 0.5),
Pattern::Runs => Mask::from_iter((0..len).map(|index| (index / 32).is_multiple_of(2))),
Pattern::Contiguous => Mask::from_slices(len, vec![(len / 4, len * 3 / 4)]),
}
}

fn bench_filter(
bencher: Bencher,
array: ArrayRef,
make_mask: impl Fn() -> Mask + Sync,
make_mask: impl Fn(usize) -> Mask + Sync,
cache_indices: bool,
) {
let len = array.len();
bencher
.with_inputs(|| {
let mask = make_mask();
let mask = make_mask(len);
if cache_indices {
let _ = mask.values().unwrap().indices();
}
Expand All @@ -100,32 +111,32 @@ fn bench_filter(
}

fn i8_array() -> ArrayRef {
PrimitiveArray::from_iter((0..LEN).map(|index| index as i8)).into_array()
PrimitiveArray::from_iter((0..len_of::<i8>()).map(|index| index as i8)).into_array()
}

fn i16_array() -> ArrayRef {
PrimitiveArray::from_iter((0..LEN).map(|index| index as i16)).into_array()
PrimitiveArray::from_iter((0..len_of::<i16>()).map(|index| index as i16)).into_array()
}

fn i32_array() -> ArrayRef {
PrimitiveArray::from_iter((0..LEN).map(|index| index as i32)).into_array()
PrimitiveArray::from_iter((0..len_of::<i32>()).map(|index| index as i32)).into_array()
}

fn i64_array() -> ArrayRef {
PrimitiveArray::from_iter((0..LEN).map(|index| index as i64)).into_array()
PrimitiveArray::from_iter((0..len_of::<i64>()).map(|index| index as i64)).into_array()
}

fn i128_array() -> ArrayRef {
DecimalArray::from_iter(
(0..LEN).map(|index| index as i128),
(0..len_of::<i128>()).map(|index| index as i128),
DecimalDType::new(19, 0),
)
.into_array()
}

fn i256_array() -> ArrayRef {
DecimalArray::from_iter(
(0..LEN).map(|index| i256::from_i128(index as i128)),
(0..len_of::<i256>()).map(|index| i256::from_i128(index as i128)),
DecimalDType::new(39, 0),
)
.into_array()
Expand All @@ -135,7 +146,7 @@ macro_rules! random_density_benchmark {
($name:ident, $array:ident) => {
#[divan::bench(args = DENSITIES)]
fn $name(bencher: Bencher, density: f64) {
bench_filter(bencher, $array(), || random_mask(density), false);
bench_filter(bencher, $array(), |len| random_mask(len, density), false);
}
};
}
Expand All @@ -149,15 +160,20 @@ random_density_benchmark!(random_i256, i256_array);

#[divan::bench(args = PATTERNS)]
fn patterns_i128(bencher: Bencher, pattern: Pattern) {
bench_filter(bencher, i128_array(), || pattern_mask(pattern), false);
bench_filter(
bencher,
i128_array(),
|len| pattern_mask(len, pattern),
false,
);
}

#[divan::bench(args = CACHED_DENSITIES)]
fn cached_indices_i32(bencher: Bencher, density: f64) {
bench_filter(bencher, i32_array(), || random_mask(density), true);
bench_filter(bencher, i32_array(), |len| random_mask(len, density), true);
}

#[divan::bench(args = CACHED_DENSITIES)]
fn cached_indices_i128(bencher: Bencher, density: f64) {
bench_filter(bencher, i128_array(), || random_mask(density), true);
bench_filter(bencher, i128_array(), |len| random_mask(len, density), true);
}
5 changes: 3 additions & 2 deletions vortex-array/benches/take_primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ fn main() {
/// Number of indices to take. The top tier is sized to keep CodSpeed simulation under 1ms.
const NUM_INDICES: &[usize] = &[1_000, 10_000, 25_000];

/// Large enough to measure both cache-resident and streaming dictionary decoding.
const GT_NUM_INDICES: &[usize] = &[1_000_000, 16_000_000];
/// One million codes is the largest input that keeps the walltime legs under the 1 ms budget. A
/// 16 million case ran for 7 ms to 11 ms and swung by up to 60% between runs of identical code.
const GT_NUM_INDICES: &[usize] = &[1_000_000];

/// Size of the source vector / dictionary values.
const VECTOR_SIZE: &[usize] = &[16, 256, 2048, 8192];
Expand Down
7 changes: 4 additions & 3 deletions vortex-array/benches/varbinview_compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ fn main() {

const ARGS: &[(usize, usize)] = &[
// (output_size, buffer_utilization_pct)
// Output sizes sized to keep CodSpeed simulation under 1ms per benchmark.
// Output sizes sized to keep CodSpeed simulation under 1ms per benchmark. Only low
// utilization is measured: at 90% there is nothing to compact, so the timed region was a
// single check that CodSpeed reported as harness overhead, moving by 20% between runs of
// identical code.
(1 << 10, 10),
(1 << 10, 90),
(1 << 11, 10),
(1 << 11, 90),
];

static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
Expand Down
Loading
Loading