Skip to content

Commit 8d4663b

Browse files
authored
perf: Use primitive accumulator for min and max, and backport CountGroupsAccumulator
Note that we avoid the upstream DF bugs on min/max with groups accumulators on float32 and float64, where, in the max case, max({-infinity}) returns `f32::MIN` or `f64::MIN`, respectively, which are *not* negative infinity.
1 parent 64ae03e commit 8d4663b

File tree

3 files changed

+500
-23
lines changed

3 files changed

+500
-23
lines changed

datafusion/src/physical_plan/expressions/count.rs

Lines changed: 212 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@
1818
//! Defines physical expressions that can evaluated at runtime during query execution
1919
2020
use std::any::Any;
21+
use std::mem::size_of;
2122
use std::sync::Arc;
2223

23-
use crate::error::Result;
24-
use crate::physical_plan::groups_accumulator::GroupsAccumulator;
25-
use crate::physical_plan::groups_accumulator_flat_adapter::GroupsAccumulatorFlatAdapter;
24+
use crate::error::{DataFusionError, Result};
25+
use crate::physical_plan::groups_accumulator::{EmitTo, GroupsAccumulator};
26+
use crate::physical_plan::null_state::accumulate_indices;
2627
use crate::physical_plan::{Accumulator, AggregateExpr, PhysicalExpr};
2728
use crate::scalar::ScalarValue;
29+
use arrow::array::{Array, ArrayData, BooleanArray, PrimitiveArray};
30+
use arrow::buffer::Buffer;
2831
use arrow::compute;
29-
use arrow::datatypes::DataType;
32+
use arrow::datatypes::{DataType, UInt64Type};
3033
use arrow::{
3134
array::{ArrayRef, UInt64Array},
3235
datatypes::Field,
@@ -99,11 +102,7 @@ impl AggregateExpr for Count {
99102
fn create_groups_accumulator(
100103
&self,
101104
) -> arrow::error::Result<Option<Box<dyn GroupsAccumulator>>> {
102-
Ok(Some(Box::new(GroupsAccumulatorFlatAdapter::<
103-
CountAccumulator,
104-
>::new(move || {
105-
Ok(CountAccumulator::new())
106-
}))))
105+
Ok(Some(Box::new(CountGroupsAccumulator::new())))
107106
}
108107

109108
fn name(&self) -> &str {
@@ -170,6 +169,210 @@ impl Accumulator for CountAccumulator {
170169
}
171170
}
172171

172+
/// An accumulator to compute the counts of [`PrimitiveArray<T>`].
173+
/// Stores values as native types, and does overflow checking
174+
///
175+
/// Unlike most other accumulators, COUNT never produces NULLs. If no
176+
/// non-null values are seen in any group the output is 0. Thus, this
177+
/// accumulator has no additional null or seen filter tracking.
178+
#[derive(Debug)]
179+
struct CountGroupsAccumulator {
180+
/// Count per group.
181+
///
182+
/// Note that in upstream this is a Vec<i64>, and the count output and intermediate data type is
183+
/// `DataType::Int64`. But here we are still using UInt64.
184+
counts: Vec<u64>,
185+
}
186+
187+
impl CountGroupsAccumulator {
188+
pub fn new() -> Self {
189+
Self { counts: vec![] }
190+
}
191+
}
192+
193+
impl GroupsAccumulator for CountGroupsAccumulator {
194+
fn update_batch(
195+
&mut self,
196+
values: &[ArrayRef],
197+
group_indices: &[usize],
198+
opt_filter: Option<&BooleanArray>,
199+
total_num_groups: usize,
200+
) -> Result<()> {
201+
assert_eq!(values.len(), 1, "single argument to update_batch");
202+
let values = &values[0];
203+
204+
// Add one to each group's counter for each non null, non
205+
// filtered value
206+
self.counts.resize(total_num_groups, 0);
207+
accumulate_indices(
208+
group_indices,
209+
values
210+
.data_ref()
211+
.null_bitmap()
212+
.as_ref()
213+
.map(|bitmap| (bitmap, values.offset(), values.len())),
214+
opt_filter,
215+
|group_index| {
216+
self.counts[group_index] += 1;
217+
},
218+
);
219+
220+
Ok(())
221+
}
222+
223+
fn merge_batch(
224+
&mut self,
225+
values: &[ArrayRef],
226+
group_indices: &[usize],
227+
opt_filter: Option<&BooleanArray>,
228+
total_num_groups: usize,
229+
) -> Result<()> {
230+
assert_eq!(values.len(), 1, "one argument to merge_batch");
231+
// first batch is counts, second is partial sums
232+
let partial_counts = match values[0]
233+
.as_any()
234+
.downcast_ref::<PrimitiveArray<UInt64Type>>()
235+
{
236+
Some(x) => x,
237+
None => {
238+
panic!("values[0] is of unexpected type {:?}, expecting UInt64Type for intermediate count batch", values[0].data_type());
239+
}
240+
};
241+
242+
// intermediate counts are always created as non null
243+
assert_eq!(partial_counts.null_count(), 0);
244+
let partial_counts = partial_counts.values();
245+
246+
// Adds the counts with the partial counts
247+
self.counts.resize(total_num_groups, 0);
248+
match opt_filter {
249+
Some(filter) => filter
250+
.iter()
251+
.zip(group_indices.iter())
252+
.zip(partial_counts.iter())
253+
.for_each(|((filter_value, &group_index), partial_count)| {
254+
if let Some(true) = filter_value {
255+
self.counts[group_index] += partial_count;
256+
}
257+
}),
258+
None => group_indices.iter().zip(partial_counts.iter()).for_each(
259+
|(&group_index, partial_count)| {
260+
self.counts[group_index] += partial_count;
261+
},
262+
),
263+
}
264+
265+
Ok(())
266+
}
267+
268+
fn evaluate(&mut self, emit_to: EmitTo) -> Result<ArrayRef> {
269+
let counts = emit_to.take_needed(&mut self.counts);
270+
271+
// Count is always non null (null inputs just don't contribute to the overall values)
272+
273+
// TODO: This copies. Ideally, don't. Note: Avoiding this memcpy had minimal effect in PrimitiveGroupsAccumulator
274+
let buffers = vec![Buffer::from_slice_ref(&counts)];
275+
276+
let data = ArrayData::new(
277+
DataType::UInt64,
278+
counts.len(),
279+
None,
280+
None,
281+
0, /* offset */
282+
buffers,
283+
vec![],
284+
);
285+
Ok(Arc::new(PrimitiveArray::<UInt64Type>::from(data)))
286+
}
287+
288+
// return arrays for counts
289+
fn state(&mut self, emit_to: EmitTo) -> Result<Vec<ArrayRef>> {
290+
let counts = emit_to.take_needed(&mut self.counts);
291+
// Backporting note: UInt64Array::from actually does copy here in old DF.
292+
let counts: PrimitiveArray<UInt64Type> = UInt64Array::from(counts); // zero copy, no nulls
293+
Ok(vec![Arc::new(counts) as ArrayRef])
294+
}
295+
296+
/// Converts an input batch directly to a state batch
297+
///
298+
/// The state of `COUNT` is always a single Int64Array (backporting note: it is a UInt64Array):
299+
/// * `1` (for non-null, non filtered values)
300+
/// * `0` (for null values)
301+
fn convert_to_state(
302+
&self,
303+
_values: &[ArrayRef],
304+
_opt_filter: Option<&BooleanArray>,
305+
) -> Result<Vec<ArrayRef>> {
306+
// convert_to_state only gets used in upstream datafusion, and we set
307+
// supports_convert_to_state to false. Because values.data_ref().offset() and the null
308+
// bitmap have differences that require care to backport, we comment this out instead.
309+
return Err(DataFusionError::NotImplemented(
310+
"Input batch conversion to state not implemented".to_owned(),
311+
));
312+
/*
313+
let values = &values[0];
314+
315+
let state_array = match (values.logical_nulls(), opt_filter) {
316+
(None, None) => {
317+
// In case there is no nulls in input and no filter, returning array of 1
318+
Arc::new(Int64Array::from_value(1, values.len()))
319+
}
320+
(Some(nulls), None) => {
321+
// If there are any nulls in input values -- casting `nulls` (true for values, false for nulls)
322+
// of input array to Int64
323+
let nulls = BooleanArray::new(nulls.into_inner(), None);
324+
compute::cast(&nulls, &DataType::Int64)?
325+
}
326+
(None, Some(filter)) => {
327+
// If there is only filter
328+
// - applying filter null mask to filter values by bitand filter values and nulls buffers
329+
// (using buffers guarantees absence of nulls in result)
330+
// - casting result of bitand to Int64 array
331+
let (filter_values, filter_nulls) = filter.clone().into_parts();
332+
333+
let state_buf = match filter_nulls {
334+
Some(filter_nulls) => &filter_values & filter_nulls.inner(),
335+
None => filter_values,
336+
};
337+
338+
let boolean_state = BooleanArray::new(state_buf, None);
339+
compute::cast(&boolean_state, &DataType::Int64)?
340+
}
341+
(Some(nulls), Some(filter)) => {
342+
// For both input nulls and filter
343+
// - applying filter null mask to filter values by bitand filter values and nulls buffers
344+
// (using buffers guarantees absence of nulls in result)
345+
// - applying values null mask to filter buffer by another bitand on filter result and
346+
// nulls from input values
347+
// - casting result to Int64 array
348+
let (filter_values, filter_nulls) = filter.clone().into_parts();
349+
350+
let filter_buf = match filter_nulls {
351+
Some(filter_nulls) => &filter_values & filter_nulls.inner(),
352+
None => filter_values,
353+
};
354+
let state_buf = &filter_buf & nulls.inner();
355+
356+
let boolean_state = BooleanArray::new(state_buf, None);
357+
compute::cast(&boolean_state, &DataType::Int64)?
358+
}
359+
};
360+
361+
Ok(vec![state_array])
362+
*/
363+
}
364+
365+
fn supports_convert_to_state(&self) -> bool {
366+
// Is set to true in upstream (as it's implemented above in upstream). But convert_to_state
367+
// is not used in this branch anyway.
368+
false
369+
}
370+
371+
fn size(&self) -> usize {
372+
self.counts.capacity() * size_of::<usize>()
373+
}
374+
}
375+
173376
#[cfg(test)]
174377
mod tests {
175378
use super::*;

0 commit comments

Comments
 (0)