Skip to content

bench: add nullable dictionary hash density benchmarks - #27008

Open
lakshaydevoo-arch wants to merge 11 commits into
apache:mainfrom
lakshaydevoo-arch:bench/26848-dictionary-null-density
Open

lakshaydevoo-arch wants to merge 11 commits into
apache:mainfrom
lakshaydevoo-arch:bench/26848-dictionary-null-density

Conversation

@lakshaydevoo-arch

Copy link
Copy Markdown

Important

Fork CI setup: For pull requests from a fork to main, enable workflows in your fork's Actions tab before you push.
See the fork CI setup guide.

Which issue does this PR close?

Part of #26848.

Related to #26966.

Rationale for this change

This PR separates the additional dictionary hashing benchmarks from the optimization in #26966, as requested during review.

Having the benchmarks in main first will allow the benchmark bot to compare the same workloads before and after the optimization.

What changes are included in this PR?

  • Add Int32 dictionary hashing benchmarks with 50% and 97% null keys.
  • Cover both single-column and multi-column hashing.
  • Keep the existing 0% and 3% null-key benchmarks unchanged.
  • Reuse deterministic null-key generation for reproducible workloads.

The benchmarks use 8,192 rows and the existing dictionary value pool.

No hashing implementation changes are included.

What is the testing strategy for this PR?

The new benchmarks are registered in the existing with_hashes Criterion benchmark suite.

The benchmark target was previously compiled successfully using:

cargo bench -p datafusion-common --bench with_hashes --no-run

The additional cases were also executed locally and by the benchmark bot as part of #26966.

This PR adds benchmark coverage only. Performance comparisons will be made against these benchmarks once they are available in the baseline.

Are there any user-facing changes?

No. This PR only adds benchmarks.

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks good, we should simplify it to make it look like the other benchmarks in the file

Comment on lines +676 to +693
fn dictionary_benchmark(c: &mut Criterion) {
let pool = StringPool::new(100, 64);
let dict = pool.typed_dictionary_array::<Int32Type>(BATCH_SIZE);
let name = "dictionary_utf8_int32";
for null_percent in [50, 97] {
let array = with_null_keys(&dict, null_percent as f32 / 100.0);
c.bench_function(&format!("{name}: single, {null_percent}% null keys"), |b| {
do_hash_test(b, std::slice::from_ref(&array));
});
c.bench_function(
&format!("{name}: multiple, {null_percent}% null keys"),
|b| {
let arrays = vec![array.clone(), array.clone(), array.clone()];
do_hash_test(b, &arrays);
},
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add this to existing benchmarks

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. i will move these cases into the existing dictionary benchmarks to keep everything together.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. I've moved the 50% and 97% null-key cases into the existing criterion_benchmark() loop and removed the separate benchmark function, the existing benchmark cases remain unchanged.

lakshaydevoo-arch and others added 3 commits November 3, 2026 19:52
Co-authored-by: RIchard Baah <137434454+Rich-T-kid@users.noreply.github.com>
Add comments for future null-density cases in benchmarks.

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @lakshaydevoo-arch

@lakshaydevoo-arch

lakshaydevoo-arch commented Nov 3, 2026 •

Copy link
Copy Markdown
Author

i have found another issue also , it's a sorting bug that when adding another column it distorts the global sorting order, should i start investigation on it in another branch
or wait for my these remaining optimization pr's to merge?

@Rich-T-kid

Copy link
Copy Markdown
Contributor

I think its best to do that in a separate branch

@lakshaydevoo-arch

Copy link
Copy Markdown
Author

I was mainly hesitant about opening another PR while my current optimization PR is still under review, as I don't want to add unnecessary review overhead for the maintainers, so will be
opening another focused PR for that issue and investigate more to that until these gets merged.

@lakshaydevoo-arch

Copy link
Copy Markdown
Author

@Rich-T-kid when will it merge?

@Rich-T-kid

Copy link
Copy Markdown
Contributor

@Rich-T-kid when will it merge?

@lakshaydevoo-arch 😅 i'm not a committer. i think @kumarUjjawal would have the most context to review these changes, if he has time

@lakshaydevoo-arch

Copy link
Copy Markdown
Author

@kumarUjjawal could you please review it

@kumarUjjawal kumarUjjawal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @lakshaydevoo-arch

Left few comments please take a look

/// Returns a random null mask where each slot is null with probability
/// `null_density`, or `None` if no slot ended up null
fn create_null_mask_with_density(len: usize, null_density: f32) -> Option<NullBuffer> {
let mut rng = make_rng();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null masks and the dictionary key choice (iter_strings) both start a fresh RNG with seed 42, so which keys get masked is tied to the random draws that picked each string.

let null_density = 0.03;
let mut builder = NullBufferBuilder::new(len);
for _ in 0..len {
if rng.random::<f32>() < null_density {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every density uses the same seed and the same < null_density test, so the null positions are nested: 3% inside 50% inside 97%.

let nullable_array = with_null_keys(dict, null_percent as f32 / 100.0);

c.bench_function(
&format!("{name}: single, {null_percent}% null keys"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cases are named '{name}: single, 50% null keys', while the existing 3% case for the same array is still named '{name}: single, nulls'.

}
// Apply additional null-density cases to all Int32 dictionary
// benchmarks, including any added in the future.
if let Some(dict) = array.as_any().downcast_ref::<DictionaryArray<Int32Type>>() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra cases are chosen by downcasting inside the shared loop instead of being declared on the BenchData entry (e.g. a null_densities: &'static [u8] field).

/// Returns `dict` with a new random null mask on its keys, where each key is null
/// with probability `null_density`. Any existing key nulls are replaced, and the
/// key and value buffers are shared rather than copied
fn with_null_keys<K: ArrowDictionaryKeyType>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_null_keys is generic over the key type K, but its only caller passes DictionaryArray<Int32Type>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common Related to common crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants