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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/contributor-guide/expression-audits/map_funcs.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
- Spark 4.0.1 (audited 2026-05-27): semantics unchanged.
- Spark 4.1.1 (audited 2026-05-27): identical to 4.0.1.

## map_sort

- Performance (tuned locally 2026-09-12; [PR #5887](https://github.com/apache/datafusion-comet/pull/5887)): skip Arrow sort dispatch for eligible flat singleton keys, with a batch check and specialized fallback loop for batches without singletons. In the local DataFusion 55.0.0 development cohort, matched singleton normalization measured 19–22x faster in the full run and 18.4x in an independent forward-order confirmation. Benchmarks: `native/spark-expr/benches/map_sort.rs`, `hash.rs`, and `common/matched_maps.rs`; 92 cases cover normalization, hashing, combined execution, nulls, slices, mixed cardinalities, and long Unicode values. Flagged regressions did not remain stable through independent and reversed-order confirmation.

## map_values

- Spark 3.4.3 (audited 2026-05-27): identical to 3.5.8.
Expand Down
123 changes: 123 additions & 0 deletions native/spark-expr/benches/common/matched_maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,126 @@ pub fn bench_maps(c: &mut Criterion, stage: Stage) {
}
group.finish();
}

/// Additional regression shapes use the same rows and hash seed as the matched fixtures.
/// Null maps retain physical entries, exercising normalization underneath null slots too.
/// The leading row is sliced away to exercise nonzero entry offsets in every case.
pub fn bench_regression_maps(c: &mut Criterion, stage: Stage) {
let ints: ArrayRef = Arc::new(Int32Array::from_iter_values((0..ROWS).map(c1)));
let mut group = c.benchmark_group(format!("regression_maps/{}", stage.name()));
group.throughput(Throughput::Elements(ROWS as u64));
for (name, null_every, mixed, long) in [
("singleton_no_null", 0, false, false),
("singleton_sparse_null", 100, false, false),
("singleton_dense_null", 2, false, false),
("mixed_no_null", 0, true, false),
("mixed_sparse_null", 100, true, false),
("mixed_dense_null", 2, true, false),
("singleton_long_unicode", 0, false, true),
("mixed_long_unicode_dense_null", 2, true, true),
] {
let mut builder = MapBuilder::new(
Some(crate::common::map_field_names()),
StringBuilder::new(),
StringBuilder::new(),
);
let prefix = if long {
"資料é".repeat(128)
} else {
String::new()
};
for row in 0..=ROWS {
let count = if row == 0 {
3 // Sliced-away prefix must have entries, even for mixed cardinalities.
} else if mixed {
[0, 1, 1, 2, 10, 50][row % 6]
} else {
1
};
for entry in (0..count).rev() {
builder.keys().append_value(format!("{prefix}{entry:04}"));
if null_every != 0 && (row + entry + 1) % null_every == 0 {
builder.values().append_null();
} else {
builder
.values()
.append_value(format!("{prefix}{row}:{entry}"));
}
}
builder
.append(null_every == 0 || row % null_every != 0)
.unwrap();
}
let raw: ArrayRef = Arc::new(builder.finish().slice(1, ROWS));
let args = [ColumnarValue::Array(Arc::clone(&raw))];
let normalized = normalize(&args);
// Independent expected permutation checks values, child nulls, and schema as well
// as ordering; explicit checks cover map validity and rebased sliced offsets.
let map = raw.as_any().downcast_ref::<MapArray>().unwrap();
let keys = map
.keys()
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
let mut permutation = Vec::new();
let mut offsets = vec![0i32];
for pair in map.value_offsets().windows(2) {
let mut indices: Vec<u32> = (pair[0] as u32..pair[1] as u32).collect();
indices.sort_by(|a, b| keys.value(*a as usize).cmp(keys.value(*b as usize)));
permutation.extend(indices);
offsets.push(permutation.len() as i32);
}
let expected_entries = arrow::compute::take(
map.entries(),
&arrow::array::UInt32Array::from(permutation),
None,
)
.unwrap();
let actual = normalized.as_any().downcast_ref::<MapArray>().unwrap();
assert_eq!(actual.entries().to_data(), expected_entries.to_data());
assert_eq!(actual.value_offsets(), offsets);
assert_eq!(actual.nulls(), map.nulls());
assert_eq!(actual.data_type(), map.data_type());
for shape in [Shape::Map, Shape::StructMapInt] {
if matches!((stage, shape), (Stage::NormalizeOnly, Shape::StructMapInt)) {
continue;
}
group.bench_function(BenchmarkId::new(shape.name(), name), |b| match stage {
Stage::NormalizeOnly => b.iter(|| black_box(normalize(black_box(&args)))),
Stage::HashOnly => {
let input = match shape {
Shape::Map => Arc::clone(&normalized),
Shape::StructMapInt => wrap(Arc::clone(&normalized), &ints),
};
let mut buffer = vec![42; ROWS];
b.iter(|| {
buffer.fill(42);
create_murmur3_hashes(std::slice::from_ref(black_box(&input)), &mut buffer)
.unwrap();
black_box(&buffer);
});
}
Stage::NormalizeHash => {
let mut buffer = vec![42; ROWS];
match shape {
Shape::Map => b.iter(|| {
buffer.fill(42);
let input = normalize(black_box(&args));
create_murmur3_hashes(std::slice::from_ref(&input), &mut buffer)
.unwrap();
black_box(&buffer);
}),
Shape::StructMapInt => b.iter(|| {
buffer.fill(42);
let input = wrap(normalize(black_box(&args)), &ints);
create_murmur3_hashes(std::slice::from_ref(&input), &mut buffer)
.unwrap();
black_box(&buffer);
}),
}
}
});
}
}
group.finish();
}
2 changes: 2 additions & 0 deletions native/spark-expr/benches/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ fn bench(c: &mut Criterion) {
fn bench_matched_maps(c: &mut Criterion) {
matched_maps::bench_maps(c, matched_maps::Stage::HashOnly);
matched_maps::bench_maps(c, matched_maps::Stage::NormalizeHash);
matched_maps::bench_regression_maps(c, matched_maps::Stage::HashOnly);
matched_maps::bench_regression_maps(c, matched_maps::Stage::NormalizeHash);
c.bench_function("matched_maps/hash_buffer_seed_reset", |b| {
let mut hashes = vec![42u32; matched_maps::ROWS];
b.iter(|| {
Expand Down
53 changes: 51 additions & 2 deletions native/spark-expr/benches/map_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn build_string_key_map(entries_per_map: usize) -> MapArray {
fn bench_map_sort(c: &mut Criterion) {
let mut group = c.benchmark_group("spark_map_sort");

for entries in [4usize, 16, 64] {
for entries in [0usize, 1, 4, 16, 64] {
let int_map: ArrayRef = Arc::new(build_int_key_map(entries));
group.bench_with_input(
BenchmarkId::new("int_keys", entries),
Expand All @@ -99,9 +99,58 @@ fn bench_map_sort(c: &mut Criterion) {
group.finish();
}

// Struct keys are rejected by Arrow even when each map has just one entry.
// Keep the fallible path in the baseline suite so type validation cannot disappear.
fn bench_unsupported_singleton(c: &mut Criterion) {
use arrow::array::{Array, Int32Array, StructArray};
use arrow::buffer::OffsetBuffer;
use arrow::datatypes::{DataType, Field};

let keys = StructArray::new(
vec![Arc::new(Field::new("k", DataType::Int32, false))].into(),
vec![Arc::new(Int32Array::from_iter_values(0..BATCH_SIZE as i32))],
None,
);
let entries = StructArray::new(
vec![
Arc::new(Field::new("key", keys.data_type().clone(), false)),
Arc::new(Field::new("value", DataType::Int32, true)),
]
.into(),
vec![
Arc::new(keys),
Arc::new(Int32Array::from(vec![1; BATCH_SIZE])),
],
None,
);
let map = MapArray::new(
Arc::new(Field::new("entries", entries.data_type().clone(), false)),
OffsetBuffer::new((0..=BATCH_SIZE as i32).collect::<Vec<_>>().into()),
entries,
None,
false,
);
let args = [ColumnarValue::Array(Arc::new(map))];
assert!(spark_map_sort(&args)
.unwrap_err()
.to_string()
.contains("Sort not supported"));
// Input creation is untimed; error creation and drop are timed. Only the first
// nonempty row is visited, so this is not full-batch throughput.
c.bench_function("spark_map_sort/unsupported_singleton_struct_key", |b| {
b.iter(|| black_box(spark_map_sort(black_box(&args)).unwrap_err()));
});
}

fn bench_matched_maps(c: &mut Criterion) {
matched_maps::bench_maps(c, matched_maps::Stage::NormalizeOnly);
matched_maps::bench_regression_maps(c, matched_maps::Stage::NormalizeOnly);
}

criterion_group!(benches, bench_map_sort, bench_matched_maps);
criterion_group!(
benches,
bench_map_sort,
bench_matched_maps,
bench_unsupported_singleton
);
criterion_main!(benches);
Loading
Loading