Initial benchmark implementation
This commit is contained in:
46
.env
Normal file
46
.env
Normal file
@@ -0,0 +1,46 @@
|
||||
# Benchmark universe and derived scenario sizes.
|
||||
MAX_VALUE=100000000
|
||||
SPARSE_SET_PERCENT=0.004
|
||||
SEMI_SPARSE_SET_PERCENT=0.04
|
||||
NORMAL_SET_PERCENT=0.4
|
||||
DENSE_SET_PERCENT=4
|
||||
|
||||
# Requested overlap percentages for the smaller set.
|
||||
LOW_OVERLAP_PERCENT=10
|
||||
MEDIUM_OVERLAP_PERCENT=50
|
||||
HIGH_OVERLAP_PERCENT=80
|
||||
|
||||
# Select which density scenarios are included.
|
||||
ENABLE_SPARSE_SCENARIO=true
|
||||
ENABLE_SEMI_SPARSE_SCENARIO=true
|
||||
ENABLE_NORMAL_SCENARIO=true
|
||||
ENABLE_DENSE_SCENARIO=true
|
||||
|
||||
# Select which overlap scenarios are included.
|
||||
ENABLE_LOW_OVERLAP=false
|
||||
ENABLE_MEDIUM_OVERLAP=true
|
||||
ENABLE_HIGH_OVERLAP=false
|
||||
|
||||
# Benchmark execution controls.
|
||||
BENCHMARK_MIN_SAMPLES=2
|
||||
BENCHMARK_MAX_SAMPLES=5
|
||||
BENCHMARK_TARGET_TOTAL_MS=800
|
||||
|
||||
# Select which algorithms are included in the benchmark run.
|
||||
ENABLE_BITSET=true
|
||||
ENABLE_SIMD_BITSET=false
|
||||
ENABLE_STD_HASH=true
|
||||
ENABLE_CUSTOM_HASH=true
|
||||
ENABLE_SORTED_MERGE=true
|
||||
|
||||
# Select which benchmark phases are emitted.
|
||||
ENABLE_PREPARE_PHASE=true
|
||||
ENABLE_INTERSECTION_PHASE=true
|
||||
|
||||
# Select how benchmark output is rendered.
|
||||
OUTPUT_FORMAT=markdown
|
||||
|
||||
# Select which extra harness steps are counted inside each timed sample.
|
||||
TIME_PREPARE_INCLUDE_INPUT_GENERATION=false
|
||||
TIME_INTERSECTION_INCLUDE_OUTPUT_CLEAR=false
|
||||
TIME_INTERSECTION_INCLUDE_RESULT_COUNT=false
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "intersection_benchmark"
|
||||
version = "0.1.0"
|
||||
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "intersection_benchmark"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
175
README.md
Normal file
175
README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Intersection Benchmark
|
||||
|
||||
This project benchmarks several set-intersection strategies in Rust over the same generated input scenarios.
|
||||
|
||||
The benchmark output is split into two timed phases:
|
||||
|
||||
- `prepare`
|
||||
- Measures the conversion from the benchmark's raw input format, a normal array of numbers, into the algorithm's prepared internal representation.
|
||||
- This phase does not measure the later intersection itself.
|
||||
- By default, raw input generation is not included in this time.
|
||||
- If `TIME_PREPARE_INCLUDE_INPUT_GENERATION=true` in `.env`, raw input generation is included too.
|
||||
|
||||
- `native`
|
||||
- Measures only the intersection step on already prepared inputs.
|
||||
- The result is written into the algorithm's native output representation.
|
||||
- This phase does not measure preparation from the raw input arrays.
|
||||
- This phase does not measure converting the native result into a plain array of numbers.
|
||||
- By default, output clearing and result counting are not included in this time.
|
||||
- If enabled in `.env`, `TIME_INTERSECTION_INCLUDE_OUTPUT_CLEAR` and `TIME_INTERSECTION_INCLUDE_RESULT_COUNT` move those extra steps into the timed window.
|
||||
|
||||
Important notes about the timing model:
|
||||
|
||||
- Output storage is created before timed `native` samples begin and then reused across samples.
|
||||
- Warmup runs are performed before measured samples and are not included in the reported timings.
|
||||
- Printing, formatting, statistics aggregation, and scenario planning are not part of the reported algorithm timings.
|
||||
|
||||
In short:
|
||||
|
||||
- `prepare` answers: how long does it take to build the algorithm's working representation?
|
||||
- `native` answers: how long does it take to compute the intersection into the algorithm's own output format?
|
||||
|
||||
# Intersection benchmark suite
|
||||
|
||||
- Scenarios: 8
|
||||
- Universe: `0..=100000000` (`100000001` values)
|
||||
- Set populations: sparse=`4000` (0.0040%) | semi-sparse=`40000` (0.0400%) | normal=`400000` (0.4000%) | dense=`4000000` (4.000%)
|
||||
- Overlap targets: low=10.0% | medium=50.0% | high=80.0%
|
||||
- Enabled densities: sparse=`true` | semi-sparse=`true` | normal=`true` | dense=`true`
|
||||
- Enabled overlaps: low=`false` | medium=`true` | high=`false`
|
||||
- Sampling: min=`2` | max=`5` | target total=`800ms`
|
||||
- Enabled algorithms: bitset=`true` | bitset-simd=`false` | std-hash=`true` | splitmix-hash=`true` | sorted-merge=`true`
|
||||
- Enabled phases: prepare=`true` | intersection=`true`
|
||||
- Timed extras: prepare input generation=`false` | intersection output clear=`false` | intersection result count=`false`
|
||||
- Phases per algorithm: prepare=`true` | native output intersection=`true`
|
||||
|
||||
## Scenario: ordered input | sparse set population = 0.0040% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `4000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`2000/4000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 411.335us | 401.487us | 393.202us | 444.185us |
|
||||
| bitset | native | 5 | 950.881us | 940.646us | 840.562us | 1.065ms |
|
||||
| std-hash | prepare | 5 | 80.875us | 80.946us | 79.932us | 81.558us |
|
||||
| std-hash | native | 5 | 76.875us | 75.985us | 74.445us | 81.305us |
|
||||
| splitmix-hash | prepare | 5 | 36.794us | 36.825us | 36.478us | 37.109us |
|
||||
| splitmix-hash | native | 5 | 35.065us | 33.227us | 30.197us | 43.628us |
|
||||
| sorted-merge | prepare | 5 | 350ns | 303ns | 295ns | 548ns |
|
||||
| sorted-merge | native | 5 | 3.568us | 3.541us | 3.533us | 3.646us |
|
||||
|
||||
## Scenario: ordered input | semi-sparse set population = 0.0400% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `40000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`20000/40000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 3.955ms | 3.951ms | 3.927ms | 4.014ms |
|
||||
| bitset | native | 5 | 1.428ms | 1.449ms | 1.356ms | 1.507ms |
|
||||
| std-hash | prepare | 5 | 864.919us | 860.986us | 854.337us | 880.726us |
|
||||
| std-hash | native | 5 | 905.199us | 902.901us | 898.793us | 913.503us |
|
||||
| splitmix-hash | prepare | 5 | 413.275us | 410.172us | 408.810us | 423.680us |
|
||||
| splitmix-hash | native | 5 | 469.869us | 467.963us | 465.287us | 477.299us |
|
||||
| sorted-merge | prepare | 5 | 6.172us | 6.124us | 5.921us | 6.359us |
|
||||
| sorted-merge | native | 5 | 36.202us | 36.045us | 36.023us | 36.815us |
|
||||
|
||||
## Scenario: ordered input | normal set population = 0.4000% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `400000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`200000/400000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 7.272ms | 7.200ms | 6.820ms | 7.893ms |
|
||||
| bitset | native | 5 | 1.830ms | 1.821ms | 1.815ms | 1.868ms |
|
||||
| std-hash | prepare | 5 | 11.484ms | 11.262ms | 10.767ms | 12.441ms |
|
||||
| std-hash | native | 5 | 15.501ms | 15.262ms | 14.570ms | 17.524ms |
|
||||
| splitmix-hash | prepare | 5 | 5.993ms | 6.124ms | 5.715ms | 6.193ms |
|
||||
| splitmix-hash | native | 5 | 5.363ms | 5.381ms | 5.333ms | 5.383ms |
|
||||
| sorted-merge | prepare | 5 | 309.850us | 265.482us | 248.158us | 507.857us |
|
||||
| sorted-merge | native | 5 | 567.072us | 543.143us | 527.883us | 649.634us |
|
||||
|
||||
## Scenario: ordered input | dense set population = 4.000% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `4000000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`2000000/4000000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 14.176ms | 13.606ms | 12.950ms | 17.112ms |
|
||||
| bitset | native | 5 | 1.765ms | 1.731ms | 1.674ms | 1.860ms |
|
||||
| std-hash | prepare | 2 | 441.875ms | 441.875ms | 436.380ms | 447.370ms |
|
||||
| std-hash | native | 2 | 445.366ms | 445.366ms | 442.813ms | 447.919ms |
|
||||
| splitmix-hash | prepare | 4 | 243.982ms | 239.375ms | 237.612ms | 259.564ms |
|
||||
| splitmix-hash | native | 5 | 51.967ms | 49.131ms | 48.321ms | 56.781ms |
|
||||
| sorted-merge | prepare | 5 | 11.852ms | 11.638ms | 11.416ms | 12.418ms |
|
||||
| sorted-merge | native | 5 | 5.537ms | 5.530ms | 5.517ms | 5.582ms |
|
||||
|
||||
## Scenario: unordered input | sparse set population = 0.0040% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `4000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`2000/4000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 2.264ms | 882.303us | 845.089us | 7.840ms |
|
||||
| bitset | native | 5 | 1.776ms | 1.778ms | 1.695ms | 1.861ms |
|
||||
| std-hash | prepare | 5 | 83.110us | 80.293us | 79.781us | 93.585us |
|
||||
| std-hash | native | 5 | 77.430us | 77.762us | 74.848us | 79.988us |
|
||||
| splitmix-hash | prepare | 5 | 36.017us | 35.957us | 35.943us | 36.129us |
|
||||
| splitmix-hash | native | 5 | 33.200us | 31.326us | 28.101us | 42.169us |
|
||||
| sorted-merge | prepare | 5 | 61.613us | 60.791us | 55.215us | 69.401us |
|
||||
| sorted-merge | native | 5 | 3.617us | 3.533us | 3.528us | 3.943us |
|
||||
|
||||
## Scenario: unordered input | semi-sparse set population = 0.0400% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `40000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`20000/40000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 2.221ms | 1.596ms | 1.463ms | 3.909ms |
|
||||
| bitset | native | 5 | 1.770ms | 1.761ms | 1.715ms | 1.829ms |
|
||||
| std-hash | prepare | 5 | 882.778us | 869.598us | 865.722us | 910.316us |
|
||||
| std-hash | native | 5 | 917.268us | 915.333us | 900.514us | 935.037us |
|
||||
| splitmix-hash | prepare | 5 | 417.845us | 420.083us | 411.847us | 422.302us |
|
||||
| splitmix-hash | native | 5 | 475.443us | 473.060us | 466.552us | 486.611us |
|
||||
| sorted-merge | prepare | 5 | 866.901us | 867.193us | 857.034us | 877.401us |
|
||||
| sorted-merge | native | 5 | 49.398us | 48.383us | 48.283us | 53.209us |
|
||||
|
||||
## Scenario: unordered input | normal set population = 0.4000% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `400000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`200000/400000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 4.712ms | 4.803ms | 4.476ms | 4.920ms |
|
||||
| bitset | native | 5 | 1.759ms | 1.756ms | 1.687ms | 1.844ms |
|
||||
| std-hash | prepare | 5 | 10.839ms | 10.826ms | 10.524ms | 11.178ms |
|
||||
| std-hash | native | 5 | 15.163ms | 14.614ms | 14.440ms | 17.563ms |
|
||||
| splitmix-hash | prepare | 5 | 5.632ms | 5.632ms | 5.585ms | 5.679ms |
|
||||
| splitmix-hash | native | 5 | 5.483ms | 5.432ms | 5.233ms | 5.969ms |
|
||||
| sorted-merge | prepare | 5 | 10.528ms | 10.457ms | 10.445ms | 10.814ms |
|
||||
| sorted-merge | native | 5 | 544.184us | 534.490us | 530.891us | 581.634us |
|
||||
|
||||
## Scenario: unordered input | dense set population = 4.000% of universe | medium overlap percentage = 50.0% of each set
|
||||
|
||||
- Set population: `4000000` / `100000001` values
|
||||
- Overlap: requested=50.0% | actual=50.0% | shared=`2000000/4000000`
|
||||
|
||||
| algorithm | phase | samples | mean | median | min | max |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| bitset | prepare | 5 | 58.153ms | 57.261ms | 53.448ms | 63.278ms |
|
||||
| bitset | native | 5 | 1.805ms | 1.782ms | 1.652ms | 1.985ms |
|
||||
| std-hash | prepare | 2 | 461.857ms | 461.857ms | 444.440ms | 479.274ms |
|
||||
| std-hash | native | 2 | 438.653ms | 438.653ms | 435.414ms | 441.891ms |
|
||||
| splitmix-hash | prepare | 4 | 252.147ms | 250.242ms | 243.913ms | 264.189ms |
|
||||
| splitmix-hash | native | 5 | 50.829ms | 49.904ms | 49.156ms | 55.716ms |
|
||||
| sorted-merge | prepare | 5 | 130.853ms | 130.469ms | 129.970ms | 132.748ms |
|
||||
| sorted-merge | native | 5 | 6.016ms | 5.942ms | 5.877ms | 6.351ms |
|
||||
|
||||
|
||||
|
||||
106
src/algorithms/bitset.rs
Normal file
106
src/algorithms/bitset.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::data::Order;
|
||||
|
||||
pub struct BitSetAlgorithm;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BitSetSet {
|
||||
words: Vec<u64>,
|
||||
universe_len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BitSetIntersectionOutput {
|
||||
words: Vec<u64>,
|
||||
universe_len: usize,
|
||||
}
|
||||
|
||||
impl IntersectionAlgorithm for BitSetAlgorithm {
|
||||
type Prepared = BitSetSet;
|
||||
type Output = BitSetIntersectionOutput;
|
||||
|
||||
const NAME: &'static str = "bitset";
|
||||
|
||||
fn prepare(input: &[u32], universe_len: usize, _order: Order) -> Self::Prepared {
|
||||
let word_count = universe_len.div_ceil(u64::BITS as usize);
|
||||
let mut words = vec![0_u64; word_count];
|
||||
|
||||
for &value in input {
|
||||
let index = value as usize;
|
||||
assert!(
|
||||
index < universe_len,
|
||||
"value {value} is outside the universe"
|
||||
);
|
||||
|
||||
let word_index = index / u64::BITS as usize;
|
||||
let bit_index = index % u64::BITS as usize;
|
||||
words[word_index] |= 1_u64 << bit_index;
|
||||
}
|
||||
|
||||
BitSetSet {
|
||||
words,
|
||||
universe_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output {
|
||||
assert_eq!(left.universe_len, right.universe_len);
|
||||
|
||||
BitSetIntersectionOutput {
|
||||
words: vec![0_u64; left.words.len()],
|
||||
universe_len: left.universe_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_output(_output: &mut Self::Output) {}
|
||||
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output) {
|
||||
assert_eq!(left.universe_len, right.universe_len);
|
||||
assert_eq!(left.universe_len, output.universe_len);
|
||||
|
||||
for ((left_word, right_word), output_word) in left
|
||||
.words
|
||||
.iter()
|
||||
.zip(&right.words)
|
||||
.zip(output.words.iter_mut())
|
||||
{
|
||||
*output_word = left_word & right_word;
|
||||
}
|
||||
}
|
||||
|
||||
fn output_len(output: &Self::Output) -> usize {
|
||||
output
|
||||
.words
|
||||
.iter()
|
||||
.map(|word| word.count_ones() as usize)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn output_values(output: &Self::Output) -> Vec<u32> {
|
||||
let mut values = Vec::with_capacity(Self::output_len(output));
|
||||
|
||||
for (word_index, &shared_word) in output.words.iter().enumerate() {
|
||||
push_shared_word(&mut values, shared_word, word_index, output.universe_len);
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
}
|
||||
|
||||
fn push_shared_word(
|
||||
output: &mut Vec<u32>,
|
||||
mut shared: u64,
|
||||
word_index: usize,
|
||||
universe_len: usize,
|
||||
) {
|
||||
while shared != 0 {
|
||||
let bit_index = shared.trailing_zeros() as usize;
|
||||
let value = word_index * u64::BITS as usize + bit_index;
|
||||
|
||||
if value < universe_len {
|
||||
output.push(value as u32);
|
||||
}
|
||||
|
||||
shared &= shared - 1;
|
||||
}
|
||||
}
|
||||
119
src/algorithms/custom_hash.rs
Normal file
119
src/algorithms/custom_hash.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{BuildHasherDefault, Hasher};
|
||||
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::data::Order;
|
||||
|
||||
pub struct CustomHashAlgorithm;
|
||||
|
||||
type SplitMixBuildHasher = BuildHasherDefault<SplitMix64Hasher>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomHashSet {
|
||||
values: HashSet<u32, SplitMixBuildHasher>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomHashIntersectionOutput {
|
||||
values: HashSet<u32, SplitMixBuildHasher>,
|
||||
}
|
||||
|
||||
impl IntersectionAlgorithm for CustomHashAlgorithm {
|
||||
type Prepared = CustomHashSet;
|
||||
type Output = CustomHashIntersectionOutput;
|
||||
|
||||
const NAME: &'static str = "splitmix-hash";
|
||||
|
||||
fn prepare(input: &[u32], _universe_len: usize, _order: Order) -> Self::Prepared {
|
||||
let mut values =
|
||||
HashSet::with_capacity_and_hasher(input.len(), SplitMixBuildHasher::default());
|
||||
values.extend(input.iter().copied());
|
||||
CustomHashSet { values }
|
||||
}
|
||||
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output {
|
||||
CustomHashIntersectionOutput {
|
||||
values: HashSet::with_capacity_and_hasher(
|
||||
left.values.len().min(right.values.len()),
|
||||
SplitMixBuildHasher::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_output(output: &mut Self::Output) {
|
||||
output.values.clear();
|
||||
}
|
||||
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output) {
|
||||
let (smaller, larger) = ordered_sets(&left.values, &right.values);
|
||||
|
||||
for &value in smaller {
|
||||
if larger.contains(&value) {
|
||||
output.values.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output_len(output: &Self::Output) -> usize {
|
||||
output.values.len()
|
||||
}
|
||||
|
||||
fn output_values(output: &Self::Output) -> Vec<u32> {
|
||||
output.values.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn ordered_sets<'a>(
|
||||
left: &'a HashSet<u32, SplitMixBuildHasher>,
|
||||
right: &'a HashSet<u32, SplitMixBuildHasher>,
|
||||
) -> (
|
||||
&'a HashSet<u32, SplitMixBuildHasher>,
|
||||
&'a HashSet<u32, SplitMixBuildHasher>,
|
||||
) {
|
||||
if left.len() <= right.len() {
|
||||
(left, right)
|
||||
} else {
|
||||
(right, left)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplitMix64Hasher {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl Hasher for SplitMix64Hasher {
|
||||
fn finish(&self) -> u64 {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
let mut state = (bytes.len() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
|
||||
for &byte in bytes {
|
||||
state ^= byte as u64;
|
||||
state = state.rotate_left(7).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
}
|
||||
|
||||
self.state = splitmix64(state);
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, value: u32) {
|
||||
self.state = splitmix64(value as u64);
|
||||
}
|
||||
|
||||
fn write_u64(&mut self, value: u64) {
|
||||
self.state = splitmix64(value);
|
||||
}
|
||||
|
||||
fn write_usize(&mut self, value: usize) {
|
||||
self.state = splitmix64(value as u64);
|
||||
}
|
||||
}
|
||||
|
||||
fn splitmix64(mut value: u64) -> u64 {
|
||||
value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
value ^ (value >> 31)
|
||||
}
|
||||
21
src/algorithms/mod.rs
Normal file
21
src/algorithms/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod bitset;
|
||||
pub mod custom_hash;
|
||||
pub mod simd_bitset;
|
||||
pub mod sorted_merge;
|
||||
pub mod std_hash;
|
||||
|
||||
use crate::data::Order;
|
||||
|
||||
pub trait IntersectionAlgorithm {
|
||||
type Prepared;
|
||||
type Output;
|
||||
|
||||
const NAME: &'static str;
|
||||
|
||||
fn prepare(input: &[u32], universe_len: usize, order: Order) -> Self::Prepared;
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output;
|
||||
fn clear_output(output: &mut Self::Output);
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output);
|
||||
fn output_len(output: &Self::Output) -> usize;
|
||||
fn output_values(output: &Self::Output) -> Vec<u32>;
|
||||
}
|
||||
230
src/algorithms/simd_bitset.rs
Normal file
230
src/algorithms/simd_bitset.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::data::Order;
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use std::arch::aarch64::{uint64x2_t, vandq_u64, vld1q_u64, vst1q_u64};
|
||||
#[cfg(target_arch = "x86")]
|
||||
use std::arch::x86::{
|
||||
__m128i, __m256i, _mm_and_si128, _mm_loadu_si128, _mm_storeu_si128, _mm256_and_si256,
|
||||
_mm256_loadu_si256, _mm256_storeu_si256,
|
||||
};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::{
|
||||
__m128i, __m256i, _mm_and_si128, _mm_loadu_si128, _mm_storeu_si128, _mm256_and_si256,
|
||||
_mm256_loadu_si256, _mm256_storeu_si256,
|
||||
};
|
||||
|
||||
pub struct SimdBitSetAlgorithm;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SimdBitSetSet {
|
||||
words: Vec<u64>,
|
||||
universe_len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SimdBitSetIntersectionOutput {
|
||||
words: Vec<u64>,
|
||||
universe_len: usize,
|
||||
}
|
||||
|
||||
impl IntersectionAlgorithm for SimdBitSetAlgorithm {
|
||||
type Prepared = SimdBitSetSet;
|
||||
type Output = SimdBitSetIntersectionOutput;
|
||||
|
||||
const NAME: &'static str = "bitset-simd";
|
||||
|
||||
fn prepare(input: &[u32], universe_len: usize, _order: Order) -> Self::Prepared {
|
||||
let word_count = universe_len.div_ceil(u64::BITS as usize);
|
||||
let mut words = vec![0_u64; word_count];
|
||||
|
||||
for &value in input {
|
||||
let index = value as usize;
|
||||
assert!(
|
||||
index < universe_len,
|
||||
"value {value} is outside the universe"
|
||||
);
|
||||
|
||||
let word_index = index / u64::BITS as usize;
|
||||
let bit_index = index % u64::BITS as usize;
|
||||
words[word_index] |= 1_u64 << bit_index;
|
||||
}
|
||||
|
||||
SimdBitSetSet {
|
||||
words,
|
||||
universe_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output {
|
||||
assert_eq!(left.universe_len, right.universe_len);
|
||||
|
||||
SimdBitSetIntersectionOutput {
|
||||
words: vec![0_u64; left.words.len()],
|
||||
universe_len: left.universe_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_output(_output: &mut Self::Output) {}
|
||||
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output) {
|
||||
assert_eq!(left.universe_len, right.universe_len);
|
||||
assert_eq!(left.universe_len, output.universe_len);
|
||||
intersect_words(&left.words, &right.words, &mut output.words);
|
||||
}
|
||||
|
||||
fn output_len(output: &Self::Output) -> usize {
|
||||
output
|
||||
.words
|
||||
.iter()
|
||||
.map(|word| word.count_ones() as usize)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn output_values(output: &Self::Output) -> Vec<u32> {
|
||||
let mut values = Vec::with_capacity(Self::output_len(output));
|
||||
|
||||
for (word_index, &shared_word) in output.words.iter().enumerate() {
|
||||
push_shared_word(&mut values, shared_word, word_index, output.universe_len);
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
fn intersect_words(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
if std::is_x86_feature_detected!("avx2") {
|
||||
unsafe {
|
||||
intersect_avx2(left, right, output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if std::is_x86_feature_detected!("sse2") {
|
||||
unsafe {
|
||||
intersect_sse2(left, right, output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
intersect_scalar(left, right, output);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
fn intersect_words(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
unsafe {
|
||||
intersect_neon(left, right, output);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
fn intersect_words(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
intersect_scalar(left, right, output);
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn intersect_avx2(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
let chunk_len = 4;
|
||||
let simd_end = left.len() / chunk_len * chunk_len;
|
||||
let mut word_index = 0;
|
||||
|
||||
while word_index < simd_end {
|
||||
let left_vector =
|
||||
unsafe { _mm256_loadu_si256(left.as_ptr().add(word_index) as *const __m256i) };
|
||||
let right_vector =
|
||||
unsafe { _mm256_loadu_si256(right.as_ptr().add(word_index) as *const __m256i) };
|
||||
let shared_vector = _mm256_and_si256(left_vector, right_vector);
|
||||
unsafe {
|
||||
_mm256_storeu_si256(
|
||||
output.as_mut_ptr().add(word_index) as *mut __m256i,
|
||||
shared_vector,
|
||||
)
|
||||
};
|
||||
|
||||
word_index += chunk_len;
|
||||
}
|
||||
|
||||
intersect_scalar(
|
||||
&left[simd_end..],
|
||||
&right[simd_end..],
|
||||
&mut output[simd_end..],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[target_feature(enable = "sse2")]
|
||||
unsafe fn intersect_sse2(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
let chunk_len = 2;
|
||||
let simd_end = left.len() / chunk_len * chunk_len;
|
||||
let mut word_index = 0;
|
||||
|
||||
while word_index < simd_end {
|
||||
let left_vector =
|
||||
unsafe { _mm_loadu_si128(left.as_ptr().add(word_index) as *const __m128i) };
|
||||
let right_vector =
|
||||
unsafe { _mm_loadu_si128(right.as_ptr().add(word_index) as *const __m128i) };
|
||||
let shared_vector = _mm_and_si128(left_vector, right_vector);
|
||||
unsafe {
|
||||
_mm_storeu_si128(
|
||||
output.as_mut_ptr().add(word_index) as *mut __m128i,
|
||||
shared_vector,
|
||||
)
|
||||
};
|
||||
|
||||
word_index += chunk_len;
|
||||
}
|
||||
|
||||
intersect_scalar(
|
||||
&left[simd_end..],
|
||||
&right[simd_end..],
|
||||
&mut output[simd_end..],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
unsafe fn intersect_neon(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
let chunk_len = 2;
|
||||
let simd_end = left.len() / chunk_len * chunk_len;
|
||||
let mut word_index = 0;
|
||||
|
||||
while word_index < simd_end {
|
||||
let left_vector: uint64x2_t = unsafe { vld1q_u64(left.as_ptr().add(word_index)) };
|
||||
let right_vector: uint64x2_t = unsafe { vld1q_u64(right.as_ptr().add(word_index)) };
|
||||
let shared_vector = vandq_u64(left_vector, right_vector);
|
||||
unsafe { vst1q_u64(output.as_mut_ptr().add(word_index), shared_vector) };
|
||||
|
||||
word_index += chunk_len;
|
||||
}
|
||||
|
||||
intersect_scalar(
|
||||
&left[simd_end..],
|
||||
&right[simd_end..],
|
||||
&mut output[simd_end..],
|
||||
);
|
||||
}
|
||||
|
||||
fn intersect_scalar(left: &[u64], right: &[u64], output: &mut [u64]) {
|
||||
for ((left_word, right_word), output_word) in left.iter().zip(right).zip(output.iter_mut()) {
|
||||
*output_word = left_word & right_word;
|
||||
}
|
||||
}
|
||||
|
||||
fn push_shared_word(
|
||||
output: &mut Vec<u32>,
|
||||
mut shared: u64,
|
||||
word_index: usize,
|
||||
universe_len: usize,
|
||||
) {
|
||||
while shared != 0 {
|
||||
let bit_index = shared.trailing_zeros() as usize;
|
||||
let value = word_index * u64::BITS as usize + bit_index;
|
||||
|
||||
if value < universe_len {
|
||||
output.push(value as u32);
|
||||
}
|
||||
|
||||
shared &= shared - 1;
|
||||
}
|
||||
}
|
||||
66
src/algorithms/sorted_merge.rs
Normal file
66
src/algorithms/sorted_merge.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::data::Order;
|
||||
|
||||
pub struct SortedMergeAlgorithm;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SortedVecSet {
|
||||
values: Vec<u32>,
|
||||
}
|
||||
|
||||
impl IntersectionAlgorithm for SortedMergeAlgorithm {
|
||||
type Prepared = SortedVecSet;
|
||||
type Output = Vec<u32>;
|
||||
|
||||
const NAME: &'static str = "sorted-merge";
|
||||
|
||||
fn prepare(input: &[u32], _universe_len: usize, order: Order) -> Self::Prepared {
|
||||
let values = match order {
|
||||
Order::Ordered => input.to_vec(),
|
||||
Order::Unordered => {
|
||||
let mut values = input.to_vec();
|
||||
values.sort_unstable();
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
SortedVecSet { values }
|
||||
}
|
||||
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output {
|
||||
Vec::with_capacity(left.values.len().min(right.values.len()))
|
||||
}
|
||||
|
||||
fn clear_output(output: &mut Self::Output) {
|
||||
output.clear();
|
||||
}
|
||||
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output) {
|
||||
intersect_impl(&left.values, &right.values, output);
|
||||
}
|
||||
|
||||
fn output_len(output: &Self::Output) -> usize {
|
||||
output.len()
|
||||
}
|
||||
|
||||
fn output_values(output: &Self::Output) -> Vec<u32> {
|
||||
output.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_impl(left: &[u32], right: &[u32], output: &mut Vec<u32>) {
|
||||
let mut left_index = 0;
|
||||
let mut right_index = 0;
|
||||
|
||||
while left_index < left.len() && right_index < right.len() {
|
||||
match left[left_index].cmp(&right[right_index]) {
|
||||
std::cmp::Ordering::Less => left_index += 1,
|
||||
std::cmp::Ordering::Greater => right_index += 1,
|
||||
std::cmp::Ordering::Equal => {
|
||||
output.push(left[left_index]);
|
||||
left_index += 1;
|
||||
right_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/algorithms/std_hash.rs
Normal file
68
src/algorithms/std_hash.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::data::Order;
|
||||
|
||||
pub struct StdHashAlgorithm;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StdHashSet {
|
||||
values: HashSet<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StdHashIntersectionOutput {
|
||||
values: HashSet<u32>,
|
||||
}
|
||||
|
||||
impl IntersectionAlgorithm for StdHashAlgorithm {
|
||||
type Prepared = StdHashSet;
|
||||
type Output = StdHashIntersectionOutput;
|
||||
|
||||
const NAME: &'static str = "std-hash";
|
||||
|
||||
fn prepare(input: &[u32], _universe_len: usize, _order: Order) -> Self::Prepared {
|
||||
let mut values = HashSet::with_capacity(input.len());
|
||||
values.extend(input.iter().copied());
|
||||
StdHashSet { values }
|
||||
}
|
||||
|
||||
fn create_output(left: &Self::Prepared, right: &Self::Prepared) -> Self::Output {
|
||||
StdHashIntersectionOutput {
|
||||
values: HashSet::with_capacity(left.values.len().min(right.values.len())),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_output(output: &mut Self::Output) {
|
||||
output.values.clear();
|
||||
}
|
||||
|
||||
fn intersect_into(left: &Self::Prepared, right: &Self::Prepared, output: &mut Self::Output) {
|
||||
let (smaller, larger) = ordered_sets(&left.values, &right.values);
|
||||
|
||||
for &value in smaller {
|
||||
if larger.contains(&value) {
|
||||
output.values.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output_len(output: &Self::Output) -> usize {
|
||||
output.values.len()
|
||||
}
|
||||
|
||||
fn output_values(output: &Self::Output) -> Vec<u32> {
|
||||
output.values.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn ordered_sets<'a>(
|
||||
left: &'a HashSet<u32>,
|
||||
right: &'a HashSet<u32>,
|
||||
) -> (&'a HashSet<u32>, &'a HashSet<u32>) {
|
||||
if left.len() <= right.len() {
|
||||
(left, right)
|
||||
} else {
|
||||
(right, left)
|
||||
}
|
||||
}
|
||||
699
src/benchmark.rs
Normal file
699
src/benchmark.rs
Normal file
@@ -0,0 +1,699 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::algorithms::bitset::BitSetAlgorithm;
|
||||
use crate::algorithms::custom_hash::CustomHashAlgorithm;
|
||||
use crate::algorithms::simd_bitset::SimdBitSetAlgorithm;
|
||||
use crate::algorithms::sorted_merge::SortedMergeAlgorithm;
|
||||
use crate::algorithms::std_hash::StdHashAlgorithm;
|
||||
use crate::data::{DatasetConfig, DatasetPlan, Density, Order, Overlap, Scenario};
|
||||
use crate::settings::{OutputFormat, settings};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MeasurementOptions {
|
||||
pub warmup_runs: usize,
|
||||
pub min_samples: usize,
|
||||
pub max_samples: usize,
|
||||
pub target_total: Duration,
|
||||
pub include_prepare_input_generation: bool,
|
||||
pub include_intersection_output_clear: bool,
|
||||
pub include_intersection_result_count: bool,
|
||||
}
|
||||
|
||||
impl Default for MeasurementOptions {
|
||||
fn default() -> Self {
|
||||
let runtime = settings();
|
||||
|
||||
Self {
|
||||
warmup_runs: 1,
|
||||
min_samples: runtime.benchmark_min_samples,
|
||||
max_samples: runtime.benchmark_max_samples,
|
||||
target_total: Duration::from_millis(runtime.benchmark_target_total_ms),
|
||||
include_prepare_input_generation: runtime.time_prepare_include_input_generation,
|
||||
include_intersection_output_clear: runtime.time_intersection_include_output_clear,
|
||||
include_intersection_result_count: runtime.time_intersection_include_result_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MeasurementOptions {
|
||||
pub fn smoke() -> Self {
|
||||
Self {
|
||||
warmup_runs: 0,
|
||||
min_samples: 1,
|
||||
max_samples: 1,
|
||||
target_total: Duration::ZERO,
|
||||
include_prepare_input_generation: false,
|
||||
include_intersection_output_clear: false,
|
||||
include_intersection_result_count: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BenchmarkConfig {
|
||||
pub dataset: DatasetConfig,
|
||||
pub measurement: MeasurementOptions,
|
||||
}
|
||||
|
||||
impl Default for BenchmarkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dataset: DatasetConfig::default(),
|
||||
measurement: MeasurementOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BenchmarkConfig {
|
||||
pub fn smoke() -> Self {
|
||||
Self {
|
||||
dataset: DatasetConfig::smoke(),
|
||||
measurement: MeasurementOptions::smoke(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BenchmarkPhase {
|
||||
Prepare,
|
||||
IntersectNativeOutput,
|
||||
}
|
||||
|
||||
impl BenchmarkPhase {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Prepare => "prepare",
|
||||
Self::IntersectNativeOutput => "native",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MeasurementStats {
|
||||
pub samples: usize,
|
||||
pub mean: Duration,
|
||||
pub median: Duration,
|
||||
pub min: Duration,
|
||||
pub max: Duration,
|
||||
}
|
||||
|
||||
impl MeasurementStats {
|
||||
fn from_samples(samples: Vec<Duration>) -> Self {
|
||||
assert!(!samples.is_empty(), "at least one sample is required");
|
||||
|
||||
let mut sorted = samples.clone();
|
||||
sorted.sort_unstable();
|
||||
|
||||
let total_nanos: u128 = samples.iter().map(|sample| sample.as_nanos()).sum();
|
||||
let mean = duration_from_nanos(total_nanos / samples.len() as u128);
|
||||
let median = if sorted.len() % 2 == 1 {
|
||||
sorted[sorted.len() / 2]
|
||||
} else {
|
||||
let middle = sorted.len() / 2;
|
||||
duration_from_nanos((sorted[middle - 1].as_nanos() + sorted[middle].as_nanos()) / 2)
|
||||
};
|
||||
|
||||
Self {
|
||||
samples: samples.len(),
|
||||
mean,
|
||||
median,
|
||||
min: sorted[0],
|
||||
max: *sorted.last().expect("sorted is non-empty"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BenchmarkRecord {
|
||||
pub scenario: Scenario,
|
||||
pub universe_len: usize,
|
||||
pub set_len: usize,
|
||||
pub set_population_percent: f64,
|
||||
pub requested_overlap: usize,
|
||||
pub actual_overlap: usize,
|
||||
pub target_overlap_percent: f64,
|
||||
pub actual_overlap_percent: f64,
|
||||
pub algorithm: &'static str,
|
||||
pub phase: BenchmarkPhase,
|
||||
pub stats: MeasurementStats,
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
run_with_config(BenchmarkConfig::default());
|
||||
}
|
||||
|
||||
pub fn run_with_config(config: BenchmarkConfig) {
|
||||
let results = collect_results(&config);
|
||||
if results.is_empty() {
|
||||
print_no_results(settings().output_format);
|
||||
return;
|
||||
}
|
||||
match settings().output_format {
|
||||
OutputFormat::Normal => print_normal_report(&config, &results),
|
||||
OutputFormat::Markdown => print_markdown_report(&config, &results),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_results(config: &BenchmarkConfig) -> Vec<BenchmarkRecord> {
|
||||
let runtime = settings();
|
||||
let mut results = Vec::with_capacity(
|
||||
Scenario::all().len() * runtime.enabled_algorithm_count() * runtime.enabled_phase_count(),
|
||||
);
|
||||
|
||||
for scenario in Scenario::all() {
|
||||
let plan = config.dataset.plan(scenario);
|
||||
|
||||
if runtime.enable_bitset {
|
||||
benchmark_algorithm::<BitSetAlgorithm>(&plan, &config.measurement, &mut results);
|
||||
}
|
||||
if runtime.enable_simd_bitset {
|
||||
benchmark_algorithm::<SimdBitSetAlgorithm>(&plan, &config.measurement, &mut results);
|
||||
}
|
||||
if runtime.enable_std_hash {
|
||||
benchmark_algorithm::<StdHashAlgorithm>(&plan, &config.measurement, &mut results);
|
||||
}
|
||||
if runtime.enable_custom_hash {
|
||||
benchmark_algorithm::<CustomHashAlgorithm>(&plan, &config.measurement, &mut results);
|
||||
}
|
||||
if runtime.enable_sorted_merge {
|
||||
benchmark_algorithm::<SortedMergeAlgorithm>(&plan, &config.measurement, &mut results);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn benchmark_algorithm<A>(
|
||||
plan: &DatasetPlan,
|
||||
measurement: &MeasurementOptions,
|
||||
output: &mut Vec<BenchmarkRecord>,
|
||||
) where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
if settings().enable_prepare_phase {
|
||||
let prepare_stats = measure_prepare::<A>(plan, measurement);
|
||||
output.push(build_record::<A>(
|
||||
plan,
|
||||
BenchmarkPhase::Prepare,
|
||||
prepare_stats,
|
||||
));
|
||||
}
|
||||
|
||||
let (left, right) = prepare_pair::<A>(plan);
|
||||
|
||||
if settings().enable_intersection_phase {
|
||||
let materialized_stats = measure_native_output::<A>(&left, &right, measurement);
|
||||
output.push(build_record::<A>(
|
||||
plan,
|
||||
BenchmarkPhase::IntersectNativeOutput,
|
||||
materialized_stats,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_record<A>(
|
||||
plan: &DatasetPlan,
|
||||
phase: BenchmarkPhase,
|
||||
stats: MeasurementStats,
|
||||
) -> BenchmarkRecord
|
||||
where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
BenchmarkRecord {
|
||||
scenario: plan.scenario,
|
||||
universe_len: plan.universe_len,
|
||||
set_len: plan.set_len,
|
||||
set_population_percent: population_percent(plan.set_len, plan.universe_len),
|
||||
requested_overlap: plan.requested_overlap,
|
||||
actual_overlap: plan.actual_overlap,
|
||||
target_overlap_percent: plan.target_overlap_percent,
|
||||
actual_overlap_percent: plan.actual_overlap_percent(),
|
||||
algorithm: A::NAME,
|
||||
phase,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
fn measure_prepare<A>(plan: &DatasetPlan, measurement: &MeasurementOptions) -> MeasurementStats
|
||||
where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
for _ in 0..measurement.warmup_runs {
|
||||
let (left, right) = prepare_pair::<A>(plan);
|
||||
black_box(&left);
|
||||
black_box(&right);
|
||||
drop(left);
|
||||
drop(right);
|
||||
}
|
||||
|
||||
let mut samples = Vec::new();
|
||||
let mut total = Duration::ZERO;
|
||||
|
||||
while samples.len() < measurement.min_samples
|
||||
|| (samples.len() < measurement.max_samples && total < measurement.target_total)
|
||||
{
|
||||
let elapsed = if measurement.include_prepare_input_generation {
|
||||
let start = Instant::now();
|
||||
let (left, right) = prepare_pair::<A>(plan);
|
||||
let elapsed = start.elapsed();
|
||||
black_box(&left);
|
||||
black_box(&right);
|
||||
drop(left);
|
||||
drop(right);
|
||||
elapsed
|
||||
} else {
|
||||
let left_raw = plan.generate_left();
|
||||
let left_start = Instant::now();
|
||||
let left = A::prepare(&left_raw, plan.universe_len, plan.scenario.order);
|
||||
let left_elapsed = left_start.elapsed();
|
||||
black_box(&left);
|
||||
drop(left_raw);
|
||||
|
||||
let right_raw = plan.generate_right();
|
||||
let right_start = Instant::now();
|
||||
let right = A::prepare(&right_raw, plan.universe_len, plan.scenario.order);
|
||||
let right_elapsed = right_start.elapsed();
|
||||
black_box(&right);
|
||||
drop(right_raw);
|
||||
|
||||
let elapsed = left_elapsed + right_elapsed;
|
||||
drop(left);
|
||||
drop(right);
|
||||
elapsed
|
||||
};
|
||||
|
||||
total += elapsed;
|
||||
samples.push(elapsed);
|
||||
}
|
||||
|
||||
MeasurementStats::from_samples(samples)
|
||||
}
|
||||
|
||||
fn prepare_pair<A>(plan: &DatasetPlan) -> (A::Prepared, A::Prepared)
|
||||
where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
let left_raw = plan.generate_left();
|
||||
let left = A::prepare(&left_raw, plan.universe_len, plan.scenario.order);
|
||||
drop(left_raw);
|
||||
|
||||
let right_raw = plan.generate_right();
|
||||
let right = A::prepare(&right_raw, plan.universe_len, plan.scenario.order);
|
||||
drop(right_raw);
|
||||
|
||||
(left, right)
|
||||
}
|
||||
|
||||
fn measure_native_output<A>(
|
||||
left: &A::Prepared,
|
||||
right: &A::Prepared,
|
||||
measurement: &MeasurementOptions,
|
||||
) -> MeasurementStats
|
||||
where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
let mut output = A::create_output(left, right);
|
||||
|
||||
for _ in 0..measurement.warmup_runs {
|
||||
A::clear_output(&mut output);
|
||||
A::intersect_into(left, right, &mut output);
|
||||
let count = A::output_len(&output);
|
||||
black_box(&output);
|
||||
black_box(count);
|
||||
}
|
||||
|
||||
let mut samples = Vec::new();
|
||||
let mut total = Duration::ZERO;
|
||||
|
||||
while samples.len() < measurement.min_samples
|
||||
|| (samples.len() < measurement.max_samples && total < measurement.target_total)
|
||||
{
|
||||
if !measurement.include_intersection_output_clear {
|
||||
A::clear_output(&mut output);
|
||||
}
|
||||
let start = Instant::now();
|
||||
if measurement.include_intersection_output_clear {
|
||||
A::clear_output(&mut output);
|
||||
}
|
||||
A::intersect_into(left, right, &mut output);
|
||||
let elapsed = if measurement.include_intersection_result_count {
|
||||
let count = A::output_len(&output);
|
||||
black_box(count);
|
||||
start.elapsed()
|
||||
} else {
|
||||
start.elapsed()
|
||||
};
|
||||
|
||||
black_box(&output);
|
||||
if !measurement.include_intersection_result_count {
|
||||
let count = A::output_len(&output);
|
||||
black_box(count);
|
||||
}
|
||||
total += elapsed;
|
||||
samples.push(elapsed);
|
||||
}
|
||||
|
||||
MeasurementStats::from_samples(samples)
|
||||
}
|
||||
|
||||
fn print_no_results(output_format: OutputFormat) {
|
||||
match output_format {
|
||||
OutputFormat::Normal => println!(
|
||||
"No benchmark records were generated. Enable at least one algorithm and one phase in .env."
|
||||
),
|
||||
OutputFormat::Markdown => println!(
|
||||
"No benchmark records were generated. Enable at least one algorithm and one phase in `.env`."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_normal_report(config: &BenchmarkConfig, results: &[BenchmarkRecord]) {
|
||||
let runtime = settings();
|
||||
|
||||
println!("Intersection benchmark suite");
|
||||
println!("Scenarios: {}", Scenario::all().len());
|
||||
println!(
|
||||
"Universe: 0..={} ({} values)",
|
||||
runtime.max_value,
|
||||
runtime.universe_len()
|
||||
);
|
||||
println!(
|
||||
"Set populations: sparse={} ({}) semi-sparse={} ({}) normal={} ({}) dense={} ({})",
|
||||
config.dataset.sparse_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.sparse_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.semi_sparse_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.semi_sparse_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.normal_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.normal_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.dense_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.dense_size,
|
||||
config.dataset.universe_len,
|
||||
))
|
||||
);
|
||||
println!(
|
||||
"Overlap targets: low={} medium={} high={}",
|
||||
format_percent(runtime.low_overlap_percent as f64),
|
||||
format_percent(runtime.medium_overlap_percent as f64),
|
||||
format_percent(runtime.high_overlap_percent as f64)
|
||||
);
|
||||
println!(
|
||||
"Enabled densities: sparse={} semi-sparse={} normal={} dense={}",
|
||||
runtime.enable_sparse_scenario,
|
||||
runtime.enable_semi_sparse_scenario,
|
||||
runtime.enable_normal_scenario,
|
||||
runtime.enable_dense_scenario
|
||||
);
|
||||
println!(
|
||||
"Enabled overlaps: low={} medium={} high={}",
|
||||
runtime.enable_low_overlap, runtime.enable_medium_overlap, runtime.enable_high_overlap
|
||||
);
|
||||
println!(
|
||||
"Sampling: min={} max={} target_total={}ms",
|
||||
config.measurement.min_samples,
|
||||
config.measurement.max_samples,
|
||||
config.measurement.target_total.as_millis()
|
||||
);
|
||||
println!(
|
||||
"Enabled algorithms: bitset={} bitset-simd={} std-hash={} splitmix-hash={} sorted-merge={}",
|
||||
runtime.enable_bitset,
|
||||
runtime.enable_simd_bitset,
|
||||
runtime.enable_std_hash,
|
||||
runtime.enable_custom_hash,
|
||||
runtime.enable_sorted_merge
|
||||
);
|
||||
println!(
|
||||
"Enabled phases: prepare={} intersection={}",
|
||||
runtime.enable_prepare_phase, runtime.enable_intersection_phase
|
||||
);
|
||||
println!(
|
||||
"Timed extras: prepare_input_generation={} intersection_output_clear={} intersection_result_count={}",
|
||||
config.measurement.include_prepare_input_generation,
|
||||
config.measurement.include_intersection_output_clear,
|
||||
config.measurement.include_intersection_result_count
|
||||
);
|
||||
println!(
|
||||
"Phases per algorithm: prepare={} native_output_intersection={}",
|
||||
runtime.enable_prepare_phase, runtime.enable_intersection_phase
|
||||
);
|
||||
println!();
|
||||
print_normal_results(results);
|
||||
}
|
||||
|
||||
fn print_normal_results(results: &[BenchmarkRecord]) {
|
||||
let mut current_scenario = None;
|
||||
|
||||
for record in results {
|
||||
if current_scenario != Some(record.scenario) {
|
||||
if current_scenario.is_some() {
|
||||
println!();
|
||||
}
|
||||
current_scenario = Some(record.scenario);
|
||||
println!("{}", "-".repeat(96));
|
||||
println!(
|
||||
"Scenario: {} | {} = {} of universe | {} = {} of each set",
|
||||
describe_order(record.scenario.order),
|
||||
describe_density(record.scenario.density),
|
||||
format_percent(record.set_population_percent),
|
||||
describe_overlap(record.scenario.overlap),
|
||||
format_percent(record.target_overlap_percent)
|
||||
);
|
||||
println!(
|
||||
" set population: {} / {} values",
|
||||
record.set_len, record.universe_len
|
||||
);
|
||||
println!(
|
||||
" overlap: requested={} actual={} shared={}/{}{}",
|
||||
format_percent(record.target_overlap_percent),
|
||||
format_percent(record.actual_overlap_percent),
|
||||
record.actual_overlap,
|
||||
record.set_len,
|
||||
if record.actual_overlap != record.requested_overlap {
|
||||
" adjusted-for-universe"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"{:<14} {:<10} {:>7} {:>12} {:>12} {:>12} {:>12}",
|
||||
"algorithm", "phase", "samples", "mean", "median", "min", "max"
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{:<14} {:<10} {:>7} {:>12} {:>12} {:>12} {:>12}",
|
||||
record.algorithm,
|
||||
record.phase.label(),
|
||||
record.stats.samples,
|
||||
format_duration(record.stats.mean),
|
||||
format_duration(record.stats.median),
|
||||
format_duration(record.stats.min),
|
||||
format_duration(record.stats.max)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_markdown_report(config: &BenchmarkConfig, results: &[BenchmarkRecord]) {
|
||||
let runtime = settings();
|
||||
|
||||
println!("# Intersection benchmark suite");
|
||||
println!();
|
||||
println!("- Scenarios: {}", Scenario::all().len());
|
||||
println!(
|
||||
"- Universe: `0..={}` (`{}` values)",
|
||||
runtime.max_value,
|
||||
runtime.universe_len()
|
||||
);
|
||||
println!(
|
||||
"- Set populations: sparse=`{}` ({}) | semi-sparse=`{}` ({}) | normal=`{}` ({}) | dense=`{}` ({})",
|
||||
config.dataset.sparse_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.sparse_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.semi_sparse_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.semi_sparse_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.normal_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.normal_size,
|
||||
config.dataset.universe_len,
|
||||
)),
|
||||
config.dataset.dense_size,
|
||||
format_percent(population_percent(
|
||||
config.dataset.dense_size,
|
||||
config.dataset.universe_len,
|
||||
))
|
||||
);
|
||||
println!(
|
||||
"- Overlap targets: low={} | medium={} | high={}",
|
||||
format_percent(runtime.low_overlap_percent as f64),
|
||||
format_percent(runtime.medium_overlap_percent as f64),
|
||||
format_percent(runtime.high_overlap_percent as f64)
|
||||
);
|
||||
println!(
|
||||
"- Enabled densities: sparse=`{}` | semi-sparse=`{}` | normal=`{}` | dense=`{}`",
|
||||
runtime.enable_sparse_scenario,
|
||||
runtime.enable_semi_sparse_scenario,
|
||||
runtime.enable_normal_scenario,
|
||||
runtime.enable_dense_scenario
|
||||
);
|
||||
println!(
|
||||
"- Enabled overlaps: low=`{}` | medium=`{}` | high=`{}`",
|
||||
runtime.enable_low_overlap, runtime.enable_medium_overlap, runtime.enable_high_overlap
|
||||
);
|
||||
println!(
|
||||
"- Sampling: min=`{}` | max=`{}` | target total=`{}ms`",
|
||||
config.measurement.min_samples,
|
||||
config.measurement.max_samples,
|
||||
config.measurement.target_total.as_millis()
|
||||
);
|
||||
println!(
|
||||
"- Enabled algorithms: bitset=`{}` | bitset-simd=`{}` | std-hash=`{}` | splitmix-hash=`{}` | sorted-merge=`{}`",
|
||||
runtime.enable_bitset,
|
||||
runtime.enable_simd_bitset,
|
||||
runtime.enable_std_hash,
|
||||
runtime.enable_custom_hash,
|
||||
runtime.enable_sorted_merge
|
||||
);
|
||||
println!(
|
||||
"- Enabled phases: prepare=`{}` | intersection=`{}`",
|
||||
runtime.enable_prepare_phase, runtime.enable_intersection_phase
|
||||
);
|
||||
println!(
|
||||
"- Timed extras: prepare input generation=`{}` | intersection output clear=`{}` | intersection result count=`{}`",
|
||||
config.measurement.include_prepare_input_generation,
|
||||
config.measurement.include_intersection_output_clear,
|
||||
config.measurement.include_intersection_result_count
|
||||
);
|
||||
println!(
|
||||
"- Phases per algorithm: prepare=`{}` | native output intersection=`{}`",
|
||||
runtime.enable_prepare_phase, runtime.enable_intersection_phase
|
||||
);
|
||||
println!();
|
||||
|
||||
let mut current_scenario = None;
|
||||
|
||||
for record in results {
|
||||
if current_scenario != Some(record.scenario) {
|
||||
if current_scenario.is_some() {
|
||||
println!();
|
||||
}
|
||||
current_scenario = Some(record.scenario);
|
||||
println!(
|
||||
"## Scenario: {} | {} = {} of universe | {} = {} of each set",
|
||||
describe_order(record.scenario.order),
|
||||
describe_density(record.scenario.density),
|
||||
format_percent(record.set_population_percent),
|
||||
describe_overlap(record.scenario.overlap),
|
||||
format_percent(record.target_overlap_percent)
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
"- Set population: `{}` / `{}` values",
|
||||
record.set_len, record.universe_len
|
||||
);
|
||||
println!(
|
||||
"- Overlap: requested={} | actual={} | shared=`{}/{}`{}",
|
||||
format_percent(record.target_overlap_percent),
|
||||
format_percent(record.actual_overlap_percent),
|
||||
record.actual_overlap,
|
||||
record.set_len,
|
||||
if record.actual_overlap != record.requested_overlap {
|
||||
" | adjusted for universe"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
println!();
|
||||
println!("| algorithm | phase | samples | mean | median | min | max |");
|
||||
println!("| --- | --- | ---: | ---: | ---: | ---: | ---: |");
|
||||
}
|
||||
|
||||
println!(
|
||||
"| {} | {} | {} | {} | {} | {} | {} |",
|
||||
record.algorithm,
|
||||
record.phase.label(),
|
||||
record.stats.samples,
|
||||
format_duration(record.stats.mean),
|
||||
format_duration(record.stats.median),
|
||||
format_duration(record.stats.min),
|
||||
format_duration(record.stats.max)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_order(order: Order) -> &'static str {
|
||||
match order {
|
||||
Order::Ordered => "ordered input",
|
||||
Order::Unordered => "unordered input",
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_density(density: Density) -> &'static str {
|
||||
match density {
|
||||
Density::Sparse => "sparse set population",
|
||||
Density::SemiSparse => "semi-sparse set population",
|
||||
Density::Normal => "normal set population",
|
||||
Density::Dense => "dense set population",
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_overlap(overlap: Overlap) -> &'static str {
|
||||
match overlap {
|
||||
Overlap::Low => "low overlap percentage",
|
||||
Overlap::Medium => "medium overlap percentage",
|
||||
Overlap::High => "high overlap percentage",
|
||||
}
|
||||
}
|
||||
|
||||
fn population_percent(set_len: usize, universe_len: usize) -> f64 {
|
||||
if universe_len == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(set_len as f64 / universe_len as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_from_nanos(nanos: u128) -> Duration {
|
||||
let seconds = nanos / 1_000_000_000;
|
||||
let subsec_nanos = (nanos % 1_000_000_000) as u32;
|
||||
Duration::new(seconds as u64, subsec_nanos)
|
||||
}
|
||||
|
||||
fn format_percent(value: f64) -> String {
|
||||
if value >= 10.0 {
|
||||
format!("{value:.1}%")
|
||||
} else if value >= 1.0 {
|
||||
format!("{value:.3}%")
|
||||
} else {
|
||||
format!("{value:.4}%")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_duration(duration: Duration) -> String {
|
||||
let seconds = duration.as_secs_f64();
|
||||
|
||||
if seconds >= 1.0 {
|
||||
format!("{seconds:.3}s")
|
||||
} else if seconds >= 0.001 {
|
||||
format!("{:.3}ms", seconds * 1_000.0)
|
||||
} else if seconds >= 0.000_001 {
|
||||
format!("{:.3}us", seconds * 1_000_000.0)
|
||||
} else {
|
||||
format!("{:.0}ns", seconds * 1_000_000_000.0)
|
||||
}
|
||||
}
|
||||
402
src/data.rs
Normal file
402
src/data.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::settings::settings;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum Order {
|
||||
Ordered,
|
||||
Unordered,
|
||||
}
|
||||
|
||||
impl Order {
|
||||
pub const ALL: [Self; 2] = [Self::Ordered, Self::Unordered];
|
||||
}
|
||||
|
||||
impl fmt::Display for Order {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Ordered => write!(f, "ordered"),
|
||||
Self::Unordered => write!(f, "unordered"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum Density {
|
||||
Sparse,
|
||||
SemiSparse,
|
||||
Normal,
|
||||
Dense,
|
||||
}
|
||||
|
||||
impl Density {
|
||||
pub const ALL: [Self; 4] = [Self::Sparse, Self::SemiSparse, Self::Normal, Self::Dense];
|
||||
}
|
||||
|
||||
impl fmt::Display for Density {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Sparse => write!(f, "sparse"),
|
||||
Self::SemiSparse => write!(f, "semi-sparse"),
|
||||
Self::Normal => write!(f, "normal"),
|
||||
Self::Dense => write!(f, "dense"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum Overlap {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
impl Overlap {
|
||||
pub const ALL: [Self; 3] = [Self::Low, Self::Medium, Self::High];
|
||||
}
|
||||
|
||||
impl fmt::Display for Overlap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Low => write!(f, "low"),
|
||||
Self::Medium => write!(f, "medium"),
|
||||
Self::High => write!(f, "high"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct Scenario {
|
||||
pub order: Order,
|
||||
pub density: Density,
|
||||
pub overlap: Overlap,
|
||||
}
|
||||
|
||||
impl Scenario {
|
||||
pub fn all() -> Vec<Self> {
|
||||
let runtime = settings();
|
||||
let mut scenarios =
|
||||
Vec::with_capacity(Order::ALL.len() * Density::ALL.len() * Overlap::ALL.len());
|
||||
|
||||
for order in Order::ALL {
|
||||
for density in Density::ALL {
|
||||
if !runtime.density_enabled(density) {
|
||||
continue;
|
||||
}
|
||||
for overlap in Overlap::ALL {
|
||||
if !runtime.overlap_enabled(overlap) {
|
||||
continue;
|
||||
}
|
||||
scenarios.push(Self {
|
||||
order,
|
||||
density,
|
||||
overlap,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scenarios
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Scenario {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}/{}", self.order, self.density, self.overlap)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RawPair {
|
||||
pub left: Vec<u32>,
|
||||
pub right: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Ratio {
|
||||
numerator: usize,
|
||||
denominator: usize,
|
||||
}
|
||||
|
||||
impl Ratio {
|
||||
const fn new(numerator: usize, denominator: usize) -> Self {
|
||||
Self {
|
||||
numerator,
|
||||
denominator,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_percent(percent: usize) -> Self {
|
||||
Self::new(percent, 100)
|
||||
}
|
||||
|
||||
fn apply(self, value: usize) -> usize {
|
||||
value.saturating_mul(self.numerator) / self.denominator
|
||||
}
|
||||
|
||||
fn as_percentage(self) -> f64 {
|
||||
(self.numerator as f64 / self.denominator as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatasetConfig {
|
||||
pub universe_len: usize,
|
||||
pub sparse_size: usize,
|
||||
pub semi_sparse_size: usize,
|
||||
pub normal_size: usize,
|
||||
pub dense_size: usize,
|
||||
low_overlap: Ratio,
|
||||
medium_overlap: Ratio,
|
||||
high_overlap: Ratio,
|
||||
}
|
||||
|
||||
impl Default for DatasetConfig {
|
||||
fn default() -> Self {
|
||||
let settings = settings();
|
||||
let universe_len = settings.universe_len();
|
||||
|
||||
Self {
|
||||
universe_len,
|
||||
sparse_size: percentage_of(universe_len, settings.sparse_set_percent),
|
||||
semi_sparse_size: percentage_of(universe_len, settings.semi_sparse_set_percent),
|
||||
normal_size: percentage_of(universe_len, settings.normal_set_percent),
|
||||
dense_size: percentage_of(universe_len, settings.dense_set_percent),
|
||||
low_overlap: Ratio::from_percent(settings.low_overlap_percent),
|
||||
medium_overlap: Ratio::from_percent(settings.medium_overlap_percent),
|
||||
high_overlap: Ratio::from_percent(settings.high_overlap_percent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DatasetConfig {
|
||||
pub fn smoke() -> Self {
|
||||
Self {
|
||||
universe_len: 101,
|
||||
sparse_size: 10,
|
||||
semi_sparse_size: 25,
|
||||
normal_size: 50,
|
||||
dense_size: 90,
|
||||
low_overlap: Ratio::new(1, 10),
|
||||
medium_overlap: Ratio::new(1, 2),
|
||||
high_overlap: Ratio::new(9, 10),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan(&self, scenario: Scenario) -> DatasetPlan {
|
||||
let set_len = self.set_size_for(scenario.density);
|
||||
assert!(
|
||||
set_len <= self.universe_len,
|
||||
"set size {set_len} exceeds universe {}",
|
||||
self.universe_len
|
||||
);
|
||||
|
||||
let requested_overlap = self.overlap_ratio_for(scenario.overlap).apply(set_len);
|
||||
let minimum_overlap = set_len.saturating_mul(2).saturating_sub(self.universe_len);
|
||||
let actual_overlap = requested_overlap.max(minimum_overlap).min(set_len);
|
||||
let left_only = set_len - actual_overlap;
|
||||
let right_only = set_len - actual_overlap;
|
||||
let total_unique = actual_overlap + left_only + right_only;
|
||||
|
||||
assert!(
|
||||
total_unique <= self.universe_len,
|
||||
"scenario {scenario} cannot fit inside the configured universe"
|
||||
);
|
||||
|
||||
let scenario_id = scenario_id(scenario);
|
||||
let modulus = self.universe_len as u64;
|
||||
let multiplier = choose_coprime_multiplier(modulus, scenario_id);
|
||||
let addend = if modulus == 0 {
|
||||
0
|
||||
} else {
|
||||
(scenario_id.wrapping_mul(0x9E37_79B9_7F4A_7C15) + 17) % modulus
|
||||
};
|
||||
|
||||
DatasetPlan {
|
||||
scenario,
|
||||
universe_len: self.universe_len,
|
||||
set_len,
|
||||
requested_overlap,
|
||||
actual_overlap,
|
||||
left_only,
|
||||
right_only,
|
||||
target_overlap_percent: self.overlap_ratio_for(scenario.overlap).as_percentage(),
|
||||
multiplier,
|
||||
addend,
|
||||
left_shuffle_seed: scenario_id ^ 0xA5A5_A5A5_DEAD_BEEF,
|
||||
right_shuffle_seed: scenario_id ^ 0x5A5A_5A5A_CAFE_BABE,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size_for(&self, density: Density) -> usize {
|
||||
match density {
|
||||
Density::Sparse => self.sparse_size,
|
||||
Density::SemiSparse => self.semi_sparse_size,
|
||||
Density::Normal => self.normal_size,
|
||||
Density::Dense => self.dense_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlap_ratio_for(&self, overlap: Overlap) -> Ratio {
|
||||
match overlap {
|
||||
Overlap::Low => self.low_overlap,
|
||||
Overlap::Medium => self.medium_overlap,
|
||||
Overlap::High => self.high_overlap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatasetPlan {
|
||||
pub scenario: Scenario,
|
||||
pub universe_len: usize,
|
||||
pub set_len: usize,
|
||||
pub requested_overlap: usize,
|
||||
pub actual_overlap: usize,
|
||||
pub left_only: usize,
|
||||
pub right_only: usize,
|
||||
pub target_overlap_percent: f64,
|
||||
multiplier: u64,
|
||||
addend: u64,
|
||||
left_shuffle_seed: u64,
|
||||
right_shuffle_seed: u64,
|
||||
}
|
||||
|
||||
impl DatasetPlan {
|
||||
pub fn generate_left(&self) -> Vec<u32> {
|
||||
let mut values = Vec::with_capacity(self.set_len);
|
||||
self.extend_segment(&mut values, 0, self.actual_overlap);
|
||||
self.extend_segment(&mut values, self.actual_overlap, self.left_only);
|
||||
self.finish(values, self.left_shuffle_seed)
|
||||
}
|
||||
|
||||
pub fn generate_right(&self) -> Vec<u32> {
|
||||
let mut values = Vec::with_capacity(self.set_len);
|
||||
self.extend_segment(&mut values, 0, self.actual_overlap);
|
||||
self.extend_segment(
|
||||
&mut values,
|
||||
self.actual_overlap + self.left_only,
|
||||
self.right_only,
|
||||
);
|
||||
self.finish(values, self.right_shuffle_seed)
|
||||
}
|
||||
|
||||
pub fn generate_pair(&self) -> RawPair {
|
||||
RawPair {
|
||||
left: self.generate_left(),
|
||||
right: self.generate_right(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn actual_overlap_percent(&self) -> f64 {
|
||||
if self.set_len == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.actual_overlap as f64 / self.set_len as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlap_was_adjusted(&self) -> bool {
|
||||
self.actual_overlap != self.requested_overlap
|
||||
}
|
||||
|
||||
fn extend_segment(&self, values: &mut Vec<u32>, start: usize, len: usize) {
|
||||
for index in start..start + len {
|
||||
values.push(self.permute_index(index));
|
||||
}
|
||||
}
|
||||
|
||||
fn permute_index(&self, index: usize) -> u32 {
|
||||
let modulus = self.universe_len as u64;
|
||||
(((self.multiplier * index as u64) + self.addend) % modulus) as u32
|
||||
}
|
||||
|
||||
fn finish(&self, mut values: Vec<u32>, seed: u64) -> Vec<u32> {
|
||||
match self.scenario.order {
|
||||
Order::Ordered => values.sort_unstable(),
|
||||
Order::Unordered => shuffle(&mut values, seed),
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
}
|
||||
|
||||
fn scenario_id(scenario: Scenario) -> u64 {
|
||||
let order = match scenario.order {
|
||||
Order::Ordered => 1_u64,
|
||||
Order::Unordered => 2_u64,
|
||||
};
|
||||
|
||||
let density = match scenario.density {
|
||||
Density::Sparse => 3_u64,
|
||||
Density::SemiSparse => 5_u64,
|
||||
Density::Normal => 7_u64,
|
||||
Density::Dense => 11_u64,
|
||||
};
|
||||
|
||||
let overlap = match scenario.overlap {
|
||||
Overlap::Low => 11_u64,
|
||||
Overlap::Medium => 13_u64,
|
||||
Overlap::High => 17_u64,
|
||||
};
|
||||
|
||||
order * 1_000 + density * 100 + overlap
|
||||
}
|
||||
|
||||
fn choose_coprime_multiplier(modulus: u64, scenario_id: u64) -> u64 {
|
||||
if modulus <= 1 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let mut candidate = (scenario_id % (modulus - 1)).saturating_add(1);
|
||||
while gcd(candidate, modulus) != 1 {
|
||||
candidate += 1;
|
||||
if candidate >= modulus {
|
||||
candidate = 1;
|
||||
}
|
||||
}
|
||||
|
||||
candidate
|
||||
}
|
||||
|
||||
fn gcd(mut left: u64, mut right: u64) -> u64 {
|
||||
while right != 0 {
|
||||
let next = left % right;
|
||||
left = right;
|
||||
right = next;
|
||||
}
|
||||
|
||||
left
|
||||
}
|
||||
|
||||
fn shuffle(values: &mut [u32], seed: u64) {
|
||||
let mut rng = SplitMix64::new(seed);
|
||||
|
||||
for index in (1..values.len()).rev() {
|
||||
let swap_index = (rng.next_u64() % (index as u64 + 1)) as usize;
|
||||
values.swap(index, swap_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn percentage_of(total: usize, percent: f64) -> usize {
|
||||
((total as f64) * (percent / 100.0)).floor() as usize
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SplitMix64 {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl SplitMix64 {
|
||||
fn new(seed: u64) -> Self {
|
||||
Self { state: seed }
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut value = self.state;
|
||||
value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
value ^ (value >> 31)
|
||||
}
|
||||
}
|
||||
7
src/lib.rs
Normal file
7
src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod algorithms;
|
||||
pub mod benchmark;
|
||||
pub mod data;
|
||||
pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
3
src/main.rs
Normal file
3
src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
intersection_benchmark::benchmark::run();
|
||||
}
|
||||
268
src/settings.rs
Normal file
268
src/settings.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static SETTINGS: OnceLock<BenchmarkSettings> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum OutputFormat {
|
||||
Normal,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for OutputFormat {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"normal" => Ok(Self::Normal),
|
||||
"markdown" => Ok(Self::Markdown),
|
||||
_ => Err("expected normal or markdown".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BenchmarkSettings {
|
||||
pub max_value: u32,
|
||||
pub sparse_set_percent: f64,
|
||||
pub semi_sparse_set_percent: f64,
|
||||
pub normal_set_percent: f64,
|
||||
pub dense_set_percent: f64,
|
||||
pub low_overlap_percent: usize,
|
||||
pub medium_overlap_percent: usize,
|
||||
pub high_overlap_percent: usize,
|
||||
pub enable_sparse_scenario: bool,
|
||||
pub enable_semi_sparse_scenario: bool,
|
||||
pub enable_normal_scenario: bool,
|
||||
pub enable_dense_scenario: bool,
|
||||
pub enable_low_overlap: bool,
|
||||
pub enable_medium_overlap: bool,
|
||||
pub enable_high_overlap: bool,
|
||||
pub benchmark_min_samples: usize,
|
||||
pub benchmark_max_samples: usize,
|
||||
pub benchmark_target_total_ms: u64,
|
||||
pub enable_bitset: bool,
|
||||
pub enable_simd_bitset: bool,
|
||||
pub enable_std_hash: bool,
|
||||
pub enable_custom_hash: bool,
|
||||
pub enable_sorted_merge: bool,
|
||||
pub enable_prepare_phase: bool,
|
||||
pub enable_intersection_phase: bool,
|
||||
pub output_format: OutputFormat,
|
||||
pub time_prepare_include_input_generation: bool,
|
||||
pub time_intersection_include_output_clear: bool,
|
||||
pub time_intersection_include_result_count: bool,
|
||||
}
|
||||
|
||||
impl BenchmarkSettings {
|
||||
pub fn universe_len(&self) -> usize {
|
||||
self.max_value as usize + 1
|
||||
}
|
||||
|
||||
pub fn enabled_algorithm_count(&self) -> usize {
|
||||
[
|
||||
self.enable_bitset,
|
||||
self.enable_simd_bitset,
|
||||
self.enable_std_hash,
|
||||
self.enable_custom_hash,
|
||||
self.enable_sorted_merge,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|enabled| *enabled)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn enabled_phase_count(&self) -> usize {
|
||||
[self.enable_prepare_phase, self.enable_intersection_phase]
|
||||
.into_iter()
|
||||
.filter(|enabled| *enabled)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn density_enabled(&self, density: crate::data::Density) -> bool {
|
||||
match density {
|
||||
crate::data::Density::Sparse => self.enable_sparse_scenario,
|
||||
crate::data::Density::SemiSparse => self.enable_semi_sparse_scenario,
|
||||
crate::data::Density::Normal => self.enable_normal_scenario,
|
||||
crate::data::Density::Dense => self.enable_dense_scenario,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlap_enabled(&self, overlap: crate::data::Overlap) -> bool {
|
||||
match overlap {
|
||||
crate::data::Overlap::Low => self.enable_low_overlap,
|
||||
crate::data::Overlap::Medium => self.enable_medium_overlap,
|
||||
crate::data::Overlap::High => self.enable_high_overlap,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&self) {
|
||||
assert!(
|
||||
self.max_value >= 1,
|
||||
"MAX_VALUE must be at least 1 so the benchmark universe is non-trivial"
|
||||
);
|
||||
assert!(
|
||||
self.benchmark_min_samples >= 1,
|
||||
"BENCHMARK_MIN_SAMPLES must be at least 1"
|
||||
);
|
||||
assert!(
|
||||
self.benchmark_max_samples >= self.benchmark_min_samples,
|
||||
"BENCHMARK_MAX_SAMPLES must be greater than or equal to BENCHMARK_MIN_SAMPLES"
|
||||
);
|
||||
assert!(
|
||||
self.enable_sparse_scenario
|
||||
|| self.enable_semi_sparse_scenario
|
||||
|| self.enable_normal_scenario
|
||||
|| self.enable_dense_scenario,
|
||||
"Enable at least one density scenario in .env"
|
||||
);
|
||||
assert!(
|
||||
self.enable_low_overlap || self.enable_medium_overlap || self.enable_high_overlap,
|
||||
"Enable at least one overlap scenario in .env"
|
||||
);
|
||||
|
||||
for (name, percent) in [
|
||||
("SPARSE_SET_PERCENT", self.sparse_set_percent),
|
||||
("SEMI_SPARSE_SET_PERCENT", self.semi_sparse_set_percent),
|
||||
("NORMAL_SET_PERCENT", self.normal_set_percent),
|
||||
("DENSE_SET_PERCENT", self.dense_set_percent),
|
||||
] {
|
||||
assert!(
|
||||
percent.is_finite() && (0.0..=100.0).contains(&percent),
|
||||
"{name} must be between 0 and 100"
|
||||
);
|
||||
}
|
||||
|
||||
for (name, percent) in [
|
||||
("LOW_OVERLAP_PERCENT", self.low_overlap_percent),
|
||||
("MEDIUM_OVERLAP_PERCENT", self.medium_overlap_percent),
|
||||
("HIGH_OVERLAP_PERCENT", self.high_overlap_percent),
|
||||
] {
|
||||
assert!(percent <= 100, "{name} must be between 0 and 100");
|
||||
}
|
||||
}
|
||||
|
||||
fn load() -> Self {
|
||||
let env_file_values = read_env_file(".env");
|
||||
|
||||
let settings = Self {
|
||||
max_value: read_required_parsed("MAX_VALUE", &env_file_values),
|
||||
sparse_set_percent: read_required_parsed("SPARSE_SET_PERCENT", &env_file_values),
|
||||
semi_sparse_set_percent: read_required_parsed(
|
||||
"SEMI_SPARSE_SET_PERCENT",
|
||||
&env_file_values,
|
||||
),
|
||||
normal_set_percent: read_required_parsed("NORMAL_SET_PERCENT", &env_file_values),
|
||||
dense_set_percent: read_required_parsed("DENSE_SET_PERCENT", &env_file_values),
|
||||
low_overlap_percent: read_required_parsed("LOW_OVERLAP_PERCENT", &env_file_values),
|
||||
medium_overlap_percent: read_required_parsed(
|
||||
"MEDIUM_OVERLAP_PERCENT",
|
||||
&env_file_values,
|
||||
),
|
||||
high_overlap_percent: read_required_parsed("HIGH_OVERLAP_PERCENT", &env_file_values),
|
||||
enable_sparse_scenario: read_required_bool("ENABLE_SPARSE_SCENARIO", &env_file_values),
|
||||
enable_semi_sparse_scenario: read_required_bool(
|
||||
"ENABLE_SEMI_SPARSE_SCENARIO",
|
||||
&env_file_values,
|
||||
),
|
||||
enable_normal_scenario: read_required_bool("ENABLE_NORMAL_SCENARIO", &env_file_values),
|
||||
enable_dense_scenario: read_required_bool("ENABLE_DENSE_SCENARIO", &env_file_values),
|
||||
enable_low_overlap: read_required_bool("ENABLE_LOW_OVERLAP", &env_file_values),
|
||||
enable_medium_overlap: read_required_bool("ENABLE_MEDIUM_OVERLAP", &env_file_values),
|
||||
enable_high_overlap: read_required_bool("ENABLE_HIGH_OVERLAP", &env_file_values),
|
||||
benchmark_min_samples: read_required_parsed("BENCHMARK_MIN_SAMPLES", &env_file_values),
|
||||
benchmark_max_samples: read_required_parsed("BENCHMARK_MAX_SAMPLES", &env_file_values),
|
||||
benchmark_target_total_ms: read_required_parsed(
|
||||
"BENCHMARK_TARGET_TOTAL_MS",
|
||||
&env_file_values,
|
||||
),
|
||||
enable_bitset: read_required_bool("ENABLE_BITSET", &env_file_values),
|
||||
enable_simd_bitset: read_required_bool("ENABLE_SIMD_BITSET", &env_file_values),
|
||||
enable_std_hash: read_required_bool("ENABLE_STD_HASH", &env_file_values),
|
||||
enable_custom_hash: read_required_bool("ENABLE_CUSTOM_HASH", &env_file_values),
|
||||
enable_sorted_merge: read_required_bool("ENABLE_SORTED_MERGE", &env_file_values),
|
||||
enable_prepare_phase: read_required_bool("ENABLE_PREPARE_PHASE", &env_file_values),
|
||||
enable_intersection_phase: read_required_bool(
|
||||
"ENABLE_INTERSECTION_PHASE",
|
||||
&env_file_values,
|
||||
),
|
||||
output_format: read_required_parsed("OUTPUT_FORMAT", &env_file_values),
|
||||
time_prepare_include_input_generation: read_required_bool(
|
||||
"TIME_PREPARE_INCLUDE_INPUT_GENERATION",
|
||||
&env_file_values,
|
||||
),
|
||||
time_intersection_include_output_clear: read_required_bool(
|
||||
"TIME_INTERSECTION_INCLUDE_OUTPUT_CLEAR",
|
||||
&env_file_values,
|
||||
),
|
||||
time_intersection_include_result_count: read_required_bool(
|
||||
"TIME_INTERSECTION_INCLUDE_RESULT_COUNT",
|
||||
&env_file_values,
|
||||
),
|
||||
};
|
||||
|
||||
settings.validate();
|
||||
settings
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings() -> &'static BenchmarkSettings {
|
||||
SETTINGS.get_or_init(BenchmarkSettings::load)
|
||||
}
|
||||
|
||||
fn read_env_file(path: impl AsRef<Path>) -> HashMap<String, String> {
|
||||
let Ok(contents) = fs::read_to_string(path) else {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
contents
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.filter_map(|line| line.split_once('='))
|
||||
.map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_required_parsed<T>(key: &str, env_file_values: &HashMap<String, String>) -> T
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
if let Ok(value) = env::var(key) {
|
||||
return value
|
||||
.parse()
|
||||
.unwrap_or_else(|error| panic!("failed to parse {key} from environment: {error}"));
|
||||
}
|
||||
|
||||
if let Some(value) = env_file_values.get(key) {
|
||||
return value
|
||||
.parse()
|
||||
.unwrap_or_else(|error| panic!("failed to parse {key} from .env: {error}"));
|
||||
}
|
||||
|
||||
panic!("missing required setting {key}; define it in .env or the environment");
|
||||
}
|
||||
|
||||
fn read_required_bool(key: &str, env_file_values: &HashMap<String, String>) -> bool {
|
||||
if let Ok(value) = env::var(key) {
|
||||
return parse_bool(key, &value, "environment");
|
||||
}
|
||||
|
||||
if let Some(value) = env_file_values.get(key) {
|
||||
return parse_bool(key, value, ".env");
|
||||
}
|
||||
|
||||
panic!("missing required setting {key}; define it in .env or the environment");
|
||||
}
|
||||
|
||||
fn parse_bool(key: &str, value: &str, source: &str) -> bool {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => true,
|
||||
"false" | "0" | "no" | "off" => false,
|
||||
_ => panic!("failed to parse {key} from {source}: expected true/false"),
|
||||
}
|
||||
}
|
||||
250
src/tests.rs
Normal file
250
src/tests.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::algorithms::IntersectionAlgorithm;
|
||||
use crate::algorithms::bitset::BitSetAlgorithm;
|
||||
use crate::algorithms::custom_hash::CustomHashAlgorithm;
|
||||
use crate::algorithms::simd_bitset::SimdBitSetAlgorithm;
|
||||
use crate::algorithms::sorted_merge::SortedMergeAlgorithm;
|
||||
use crate::algorithms::std_hash::StdHashAlgorithm;
|
||||
use crate::benchmark::{BenchmarkConfig, collect_results};
|
||||
use crate::data::{DatasetConfig, Density, Order, Overlap, Scenario};
|
||||
use crate::settings::settings;
|
||||
|
||||
#[test]
|
||||
fn algorithms_handle_empty_sets() {
|
||||
assert_case_for_all_algorithms(&[], &[], &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithms_handle_disjoint_sets() {
|
||||
assert_case_for_all_algorithms(&[1, 3, 5], &[2, 4, 6], &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithms_handle_full_overlap() {
|
||||
assert_case_for_all_algorithms(&[1, 2, 3], &[1, 2, 3], &[1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithms_handle_single_shared_value() {
|
||||
assert_case_for_all_algorithms(&[10, 20, 30], &[5, 20, 25], &[20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithms_handle_boundary_values() {
|
||||
let max_value = settings().max_value;
|
||||
assert_case_for_all_algorithms(&[0, 1, max_value], &[0, max_value], &[0, max_value]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_produces_exact_sizes_and_overlap() {
|
||||
let config = DatasetConfig::smoke();
|
||||
let scenario = Scenario {
|
||||
order: Order::Ordered,
|
||||
density: Density::Normal,
|
||||
overlap: Overlap::Medium,
|
||||
};
|
||||
let plan = config.plan(scenario);
|
||||
let pair = plan.generate_pair();
|
||||
|
||||
assert_eq!(pair.left.len(), plan.set_len);
|
||||
assert_eq!(pair.right.len(), plan.set_len);
|
||||
assert_eq!(
|
||||
intersection_size(&pair.left, &pair.right),
|
||||
plan.actual_overlap
|
||||
);
|
||||
assert!(pair.left.windows(2).all(|window| window[0] <= window[1]));
|
||||
assert!(pair.right.windows(2).all(|window| window[0] <= window[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_preserves_uniqueness_and_unordered_shape() {
|
||||
let config = DatasetConfig::smoke();
|
||||
let scenario = Scenario {
|
||||
order: Order::Unordered,
|
||||
density: Density::SemiSparse,
|
||||
overlap: Overlap::High,
|
||||
};
|
||||
let plan = config.plan(scenario);
|
||||
let pair = plan.generate_pair();
|
||||
|
||||
assert_eq!(
|
||||
pair.left.iter().copied().collect::<BTreeSet<_>>().len(),
|
||||
pair.left.len()
|
||||
);
|
||||
assert_eq!(
|
||||
pair.right.iter().copied().collect::<BTreeSet<_>>().len(),
|
||||
pair.right.len()
|
||||
);
|
||||
assert!(!pair.left.windows(2).all(|window| window[0] <= window[1]));
|
||||
assert!(!pair.right.windows(2).all(|window| window[0] <= window[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_adjusts_impossible_overlap_levels() {
|
||||
let config = DatasetConfig::smoke();
|
||||
let scenario = Scenario {
|
||||
order: Order::Ordered,
|
||||
density: Density::Dense,
|
||||
overlap: Overlap::Low,
|
||||
};
|
||||
let plan = config.plan(scenario);
|
||||
let set_len = config.dense_size;
|
||||
let requested_overlap = set_len / 10;
|
||||
let minimum_overlap = set_len
|
||||
.saturating_mul(2)
|
||||
.saturating_sub(config.universe_len);
|
||||
|
||||
assert!(plan.overlap_was_adjusted());
|
||||
assert_eq!(plan.requested_overlap, requested_overlap);
|
||||
assert_eq!(plan.actual_overlap, requested_overlap.max(minimum_overlap));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_algorithms_match_on_every_smoke_scenario() {
|
||||
let config = DatasetConfig::smoke();
|
||||
|
||||
for scenario in Scenario::all() {
|
||||
let plan = config.plan(scenario);
|
||||
let pair = plan.generate_pair();
|
||||
let expected_values = normalized_intersection(&pair.left, &pair.right);
|
||||
let expected_count = expected_values.len();
|
||||
|
||||
assert_algorithm_matches::<BitSetAlgorithm>(
|
||||
&pair.left,
|
||||
&pair.right,
|
||||
plan.universe_len,
|
||||
&expected_values,
|
||||
expected_count,
|
||||
);
|
||||
assert_algorithm_matches::<SimdBitSetAlgorithm>(
|
||||
&pair.left,
|
||||
&pair.right,
|
||||
plan.universe_len,
|
||||
&expected_values,
|
||||
expected_count,
|
||||
);
|
||||
assert_algorithm_matches::<StdHashAlgorithm>(
|
||||
&pair.left,
|
||||
&pair.right,
|
||||
plan.universe_len,
|
||||
&expected_values,
|
||||
expected_count,
|
||||
);
|
||||
assert_algorithm_matches::<CustomHashAlgorithm>(
|
||||
&pair.left,
|
||||
&pair.right,
|
||||
plan.universe_len,
|
||||
&expected_values,
|
||||
expected_count,
|
||||
);
|
||||
assert_algorithm_matches::<SortedMergeAlgorithm>(
|
||||
&pair.left,
|
||||
&pair.right,
|
||||
plan.universe_len,
|
||||
&expected_values,
|
||||
expected_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benchmark_runner_smoke_test_returns_every_result_group() {
|
||||
let results = collect_results(&BenchmarkConfig::smoke());
|
||||
let runtime = settings();
|
||||
let expected =
|
||||
Scenario::all().len() * runtime.enabled_algorithm_count() * runtime.enabled_phase_count();
|
||||
|
||||
assert_eq!(results.len(), expected);
|
||||
}
|
||||
|
||||
fn assert_case_for_all_algorithms(left: &[u32], right: &[u32], expected: &[u32]) {
|
||||
let mut expected_values = expected.to_vec();
|
||||
expected_values.sort_unstable();
|
||||
let universe_len = left
|
||||
.iter()
|
||||
.chain(right.iter())
|
||||
.copied()
|
||||
.max()
|
||||
.map(|value| value as usize + 1)
|
||||
.unwrap_or(1);
|
||||
|
||||
assert_algorithm_matches::<BitSetAlgorithm>(
|
||||
left,
|
||||
right,
|
||||
universe_len,
|
||||
&expected_values,
|
||||
expected_values.len(),
|
||||
);
|
||||
assert_algorithm_matches::<SimdBitSetAlgorithm>(
|
||||
left,
|
||||
right,
|
||||
universe_len,
|
||||
&expected_values,
|
||||
expected_values.len(),
|
||||
);
|
||||
assert_algorithm_matches::<StdHashAlgorithm>(
|
||||
left,
|
||||
right,
|
||||
universe_len,
|
||||
&expected_values,
|
||||
expected_values.len(),
|
||||
);
|
||||
assert_algorithm_matches::<CustomHashAlgorithm>(
|
||||
left,
|
||||
right,
|
||||
universe_len,
|
||||
&expected_values,
|
||||
expected_values.len(),
|
||||
);
|
||||
assert_algorithm_matches::<SortedMergeAlgorithm>(
|
||||
left,
|
||||
right,
|
||||
universe_len,
|
||||
&expected_values,
|
||||
expected_values.len(),
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_algorithm_matches<A>(
|
||||
left: &[u32],
|
||||
right: &[u32],
|
||||
universe_len: usize,
|
||||
expected_values: &[u32],
|
||||
expected_count: usize,
|
||||
) where
|
||||
A: IntersectionAlgorithm,
|
||||
{
|
||||
let prepared_left = A::prepare(left, universe_len, infer_order(left));
|
||||
let prepared_right = A::prepare(right, universe_len, infer_order(right));
|
||||
let mut output = A::create_output(&prepared_left, &prepared_right);
|
||||
A::clear_output(&mut output);
|
||||
A::intersect_into(&prepared_left, &prepared_right, &mut output);
|
||||
assert_eq!(A::output_len(&output), expected_count);
|
||||
let mut actual_values = A::output_values(&output);
|
||||
actual_values.sort_unstable();
|
||||
assert_eq!(actual_values, expected_values);
|
||||
}
|
||||
|
||||
fn normalized_intersection(left: &[u32], right: &[u32]) -> Vec<u32> {
|
||||
let right_values = right.iter().copied().collect::<BTreeSet<_>>();
|
||||
let mut values = left
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|value| right_values.contains(value))
|
||||
.collect::<Vec<_>>();
|
||||
values.sort_unstable();
|
||||
values
|
||||
}
|
||||
|
||||
fn intersection_size(left: &[u32], right: &[u32]) -> usize {
|
||||
normalized_intersection(left, right).len()
|
||||
}
|
||||
|
||||
fn infer_order(values: &[u32]) -> Order {
|
||||
if values.windows(2).all(|window| window[0] <= window[1]) {
|
||||
Order::Ordered
|
||||
} else {
|
||||
Order::Unordered
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user