in arrow-array/src/array/union_array.rs [777:893]
fn logical_nulls(&self) -> Option<NullBuffer> {
let fields = match self.data_type() {
DataType::Union(fields, _) => fields,
_ => unreachable!(),
};
if fields.len() <= 1 {
return self
.fields
.iter()
.flatten()
.map(Array::logical_nulls)
.next()
.flatten();
}
let logical_nulls = self.fields_logical_nulls();
if logical_nulls.is_empty() {
return None;
}
let fully_null_count = logical_nulls
.iter()
.filter(|(_, nulls)| nulls.null_count() == nulls.len())
.count();
if fully_null_count == fields.len() {
if let Some((_, exactly_sized)) = logical_nulls
.iter()
.find(|(_, nulls)| nulls.len() == self.len())
{
return Some(exactly_sized.clone());
}
if let Some((_, bigger)) = logical_nulls
.iter()
.find(|(_, nulls)| nulls.len() > self.len())
{
return Some(bigger.slice(0, self.len()));
}
return Some(NullBuffer::new_null(self.len()));
}
let boolean_buffer = match &self.offsets {
Some(_) => self.gather_nulls(logical_nulls),
None => {
// Choose the fastest way to compute the logical nulls
// Gather computes one null per iteration, while the others work on 64 nulls chunks,
// but must also compute selection masks, which is expensive,
// so it's cost is the number of selection masks computed per chunk
// Since computing the selection mask gets auto-vectorized, it's performance depends on which simd feature is enabled
// For gather, the cost is the threshold where masking becomes slower than gather, which is determined with benchmarks
// TODO: bench on avx512f(feature is still unstable)
let gather_relative_cost = if cfg!(target_feature = "avx2") {
10
} else if cfg!(target_feature = "sse4.1") {
3
} else if cfg!(target_arch = "x86") || cfg!(target_arch = "x86_64") {
// x86 baseline includes sse2
2
} else {
// TODO: bench on non x86
// Always use gather on non benchmarked archs because even though it may slower on some cases,
// it's performance depends only on the union length, without being affected by the number of fields
0
};
let strategies = [
(SparseStrategy::Gather, gather_relative_cost, true),
(
SparseStrategy::MaskAllFieldsWithNullsSkipOne,
fields.len() - 1,
fields.len() == logical_nulls.len(),
),
(
SparseStrategy::MaskSkipWithoutNulls,
logical_nulls.len(),
true,
),
(
SparseStrategy::MaskSkipFullyNull,
fields.len() - fully_null_count,
true,
),
];
let (strategy, _, _) = strategies
.iter()
.filter(|(_, _, applicable)| *applicable)
.min_by_key(|(_, cost, _)| cost)
.unwrap();
match strategy {
SparseStrategy::Gather => self.gather_nulls(logical_nulls),
SparseStrategy::MaskAllFieldsWithNullsSkipOne => {
self.mask_sparse_all_with_nulls_skip_one(logical_nulls)
}
SparseStrategy::MaskSkipWithoutNulls => {
self.mask_sparse_skip_without_nulls(logical_nulls)
}
SparseStrategy::MaskSkipFullyNull => {
self.mask_sparse_skip_fully_null(logical_nulls)
}
}
}
};
let null_buffer = NullBuffer::from(boolean_buffer);
if null_buffer.null_count() > 0 {
Some(null_buffer)
} else {
None
}
}