Skip to main content

parquet/column/writer/
min_max.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::basic::SortOrder;
19use crate::column::writer::{compare_greater, is_nan};
20use crate::data_type::private::ParquetValueType;
21use crate::schema::types::BasicTypeInfo;
22
23/// Accumulators for 64 bytes of values, i.e. a cache line and one AVX-512
24/// register, or four NEON / two AVX2 registers.
25const LANES_32: usize = 16;
26const LANES_64: usize = 8;
27
28/// Returns the minimum and maximum of `values` after mapping them with `key`,
29/// or `None` if `values` is empty.
30#[inline(always)]
31fn min_max_by_key<T: Copy, K: Copy + Ord, const LANES: usize>(
32    values: &[T],
33    key: impl Fn(T) -> K,
34    key_min: K,
35    key_max: K,
36) -> Option<(K, K)> {
37    if values.is_empty() {
38        return None;
39    }
40
41    let mut mins = [key_max; LANES];
42    let mut maxs = [key_min; LANES];
43
44    let (chunks, remainder) = values.as_chunks::<LANES>();
45    for chunk in chunks {
46        for i in 0..LANES {
47            let k = key(chunk[i]);
48            mins[i] = mins[i].min(k);
49            maxs[i] = maxs[i].max(k);
50        }
51    }
52    for (i, v) in remainder.iter().enumerate() {
53        let k = key(*v);
54        mins[i] = mins[i].min(k);
55        maxs[i] = maxs[i].max(k);
56    }
57
58    let min = mins.into_iter().min().unwrap();
59    let max = maxs.into_iter().max().unwrap();
60    Some((min, max))
61}
62
63#[inline]
64fn is_unsigned(basic_type_info: &BasicTypeInfo) -> bool {
65    matches!(basic_type_info.sort_order(), SortOrder::UNSIGNED)
66}
67
68/// Returns the min and max of an `INT32` column, or `None` if `values` is empty.
69pub(crate) fn min_max_i32(basic_type_info: &BasicTypeInfo, values: &[i32]) -> Option<(i32, i32)> {
70    if is_unsigned(basic_type_info) {
71        min_max_by_key::<_, _, LANES_32>(values, |v| v as u32, u32::MIN, u32::MAX)
72            .map(|(min, max)| (min as i32, max as i32))
73    } else {
74        min_max_by_key::<_, _, LANES_32>(values, |v| v, i32::MIN, i32::MAX)
75    }
76}
77
78/// Returns the min and max of an `INT64` column, or `None` if `values` is empty.
79pub(crate) fn min_max_i64(basic_type_info: &BasicTypeInfo, values: &[i64]) -> Option<(i64, i64)> {
80    if is_unsigned(basic_type_info) {
81        min_max_by_key::<_, _, LANES_64>(values, |v| v as u64, u64::MIN, u64::MAX)
82            .map(|(min, max)| (min as i64, max as i64))
83    } else {
84        min_max_by_key::<_, _, LANES_64>(values, |v| v, i64::MIN, i64::MAX)
85    }
86}
87
88macro_rules! float_min_max {
89    ($name:ident, $float:ty, $int:ty, $uint:ty, $lanes:expr) => {
90        /// Returns the min, max and NaN count of a floating point column,
91        /// comparing by IEEE 754 total order and skipping NaN.
92        ///
93        /// Returns `None` if `values` is empty or contains only NaN, in which
94        /// case the min and max are NaN themselves and the caller falls back
95        /// to the scalar implementation.
96        pub(crate) fn $name(values: &[$float]) -> Option<($float, $float, u64)> {
97            const SIGN_SHIFT: u32 = <$int>::BITS - 1;
98
99            // Maps the bits of a float to an integer with the same ordering as
100            // `total_cmp`: negative values have their magnitude bits flipped
101            // so that they order in reverse. The mapping is its own inverse.
102            //
103            // To avoid calling `{f32,f64}::total_cmp` for both min and max values and every other comparison
104            // we transform the input once (like what rust is doing internally before comparing) and reuse it, this avoids the overhead of calling `total_cmp` twice for each value.
105            // and doing the same calculation.
106            //
107            // This is the same transformation used in Rust `f32::total_cmp` and `f64::total_cmp`
108            // but we avoid calling
109            // f32: https://github.com/rust-lang/rust/blob/b5427502dab50cb27d9327d2893ddf2337dd4388/library/core/src/num/f32.rs#L1626
110            // f64: https://github.com/rust-lang/rust/blob/b5427502dab50cb27d9327d2893ddf2337dd4388/library/core/src/num/f64.rs#L1604
111            #[inline(always)]
112            fn total_order_key(bits: $int) -> $int {
113                bits ^ ((((bits >> SIGN_SHIFT) as $uint) >> 1) as $int)
114            }
115
116            // Bounds the per lane NaN counters well below overflow on every
117            // platform, including 32-bit.
118            const BLOCK: usize = 1 << 30;
119
120            let mut min = <$int>::MAX;
121            let mut max = <$int>::MIN;
122            let mut nan_count = 0u64;
123
124            for block in values.chunks(BLOCK) {
125                let mut mins = [<$int>::MAX; $lanes];
126                let mut maxs = [<$int>::MIN; $lanes];
127                let mut nans = [<$uint>::MIN; $lanes];
128
129                // NaN lanes are replaced by the identity of each accumulator, so
130                // they never win, without a branch.
131                let mut accumulate = |i: usize, v: $float| {
132                    let bits = v.to_bits();
133                    let is_nan = v.is_nan();
134                    let key = total_order_key(bits as $int);
135                    mins[i] = mins[i].min(if is_nan { <$int>::MAX } else { key });
136                    maxs[i] = maxs[i].max(if is_nan { <$int>::MIN } else { key });
137                    nans[i] += is_nan as $uint;
138                };
139
140                let (chunks, remainder) = block.as_chunks::<$lanes>();
141                for chunk in chunks {
142                    for i in 0..$lanes {
143                        accumulate(i, chunk[i]);
144                    }
145                }
146                for (i, v) in remainder.iter().enumerate() {
147                    accumulate(i, *v);
148                }
149
150                min = min.min(mins.into_iter().min().unwrap());
151                max = max.max(maxs.into_iter().max().unwrap());
152                nan_count += nans.into_iter().map(u64::from).sum::<u64>();
153            }
154
155            if nan_count as usize == values.len() {
156                return None;
157            }
158
159            let min = <$float>::from_bits(total_order_key(min) as $uint);
160            let max = <$float>::from_bits(total_order_key(max) as $uint);
161            Some((min, max, nan_count))
162        }
163    };
164}
165
166float_min_max!(min_max_f32, f32, i32, u32, LANES_32);
167float_min_max!(min_max_f64, f64, i64, u64, LANES_64);
168
169// Get min and max values for all values in `iter`.
170//
171// For floating point we need to compare NaN values until we encounter a non-NaN
172// value which then becomes the new min/max. After this, only non-NaN values are
173// evaluated. If all values are NaN, then the min/max NaNs as determined by
174// IEEE 754 total order are returned.
175pub(crate) fn fallback_min_max<'a, T, I>(
176    basic_type_info: &BasicTypeInfo,
177    mut iter: I,
178) -> Option<(T, T, u64)>
179where
180    T: ParquetValueType + 'a,
181    I: Iterator<Item = &'a T>,
182{
183    let first = iter.next()?;
184    let mut min_max_nan = is_nan(basic_type_info, first);
185    let mut nan_count = min_max_nan as u64;
186
187    let mut min = first;
188    let mut max = first;
189    for val in iter {
190        match (min_max_nan, is_nan(basic_type_info, val)) {
191            // skip NaNs if we've encounter non-NaN
192            (false, true) => {
193                nan_count += 1;
194            }
195            // if min/max are NaN, check for non-NaN and reset
196            (true, false) => {
197                min = val;
198                max = val;
199                min_max_nan = false;
200            }
201            // both are NaN or non-NaN, so do the comparison
202            (_, val_is_nan) => {
203                nan_count += val_is_nan as u64;
204                // we've already initialized min and max, so a single value can't be both
205                // extremes
206                if compare_greater(basic_type_info, min, val) {
207                    min = val;
208                } else if compare_greater(basic_type_info, val, max) {
209                    max = val;
210                }
211            }
212        }
213    }
214
215    Some((min.clone(), max.clone(), nan_count))
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::basic::{ConvertedType, IntType, LogicalType, Type as PhysicalType};
222    use crate::schema::types::Type;
223    use rand::prelude::*;
224
225    fn type_info(physical: PhysicalType, logical: Option<LogicalType>) -> BasicTypeInfo {
226        Type::primitive_type_builder("col", physical)
227            .with_logical_type(logical)
228            .build()
229            .unwrap()
230            .get_basic_info()
231            .clone()
232    }
233
234    fn signed_int(bits: u8) -> Option<LogicalType> {
235        Some(LogicalType::Integer(IntType {
236            bit_width: bits as i8,
237            is_signed: true,
238        }))
239    }
240
241    fn unsigned_int(bits: u8) -> Option<LogicalType> {
242        Some(LogicalType::Integer(IntType {
243            bit_width: bits as i8,
244            is_signed: false,
245        }))
246    }
247
248    /// Lengths around every lane count and remainder boundary
249    fn lengths() -> impl Iterator<Item = usize> {
250        (0..=70).chain([127, 128, 129, 1000, 4097])
251    }
252
253    fn check_ints<T, F>(
254        rng: &mut StdRng,
255        info: &BasicTypeInfo,
256        kernel: F,
257        gen_value: impl Fn(&mut StdRng) -> T,
258    ) where
259        T: crate::data_type::private::ParquetValueType + Copy,
260        F: Fn(&BasicTypeInfo, &[T]) -> Option<(T, T)>,
261    {
262        for len in lengths() {
263            let values: Vec<T> = (0..len).map(|_| gen_value(rng)).collect();
264            let expected = fallback_min_max(info, values.iter());
265            let actual = kernel(info, &values).map(|(min, max)| (min, max, 0));
266            assert_eq!(actual, expected, "len {len}: {values:?}");
267        }
268    }
269
270    #[test]
271    fn test_min_max_i32() {
272        let mut rng = StdRng::seed_from_u64(42);
273        let edges = [i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX];
274        for info in [
275            type_info(PhysicalType::INT32, None),
276            type_info(PhysicalType::INT32, signed_int(32)),
277            type_info(PhysicalType::INT32, unsigned_int(32)),
278            type_info(PhysicalType::INT32, unsigned_int(8)),
279        ] {
280            check_ints(&mut rng, &info, min_max_i32, |r| r.random());
281            check_ints(&mut rng, &info, min_max_i32, |r| *edges.choose(r).unwrap());
282            check_ints(&mut rng, &info, min_max_i32, |r| r.random_range(-3..3));
283        }
284    }
285
286    #[test]
287    fn test_min_max_i64() {
288        let mut rng = StdRng::seed_from_u64(42);
289        let edges = [i64::MIN, i64::MIN + 1, -1, 0, 1, i64::MAX - 1, i64::MAX];
290        for info in [
291            type_info(PhysicalType::INT64, None),
292            type_info(PhysicalType::INT64, signed_int(64)),
293            type_info(PhysicalType::INT64, unsigned_int(64)),
294        ] {
295            check_ints(&mut rng, &info, min_max_i64, |r| r.random());
296            check_ints(&mut rng, &info, min_max_i64, |r| *edges.choose(r).unwrap());
297            check_ints(&mut rng, &info, min_max_i64, |r| r.random_range(-3..3));
298        }
299    }
300
301    #[test]
302    fn test_unsigned_int32_converted_type() {
303        let info = Type::primitive_type_builder("col", PhysicalType::INT32)
304            .with_converted_type(ConvertedType::UINT_32)
305            .build()
306            .unwrap()
307            .get_basic_info()
308            .clone();
309        assert_eq!(min_max_i32(&info, &[-1, 0, 5]), Some((0, -1)));
310    }
311
312    macro_rules! float_tests {
313        ($test:ident, $float:ty, $uint:ty, $kernel:ident, $physical:expr) => {
314            #[test]
315            fn $test() {
316                let info = type_info($physical, None);
317                let mut rng = StdRng::seed_from_u64(42);
318
319                let quiet_nan = <$float>::NAN;
320                let neg_nan = -<$float>::NAN;
321                let payload_nan = <$float>::from_bits(<$float>::NAN.to_bits() | 1);
322                let max_nan = <$float>::from_bits(<$uint>::MAX >> 1);
323                let edges = [
324                    <$float>::NEG_INFINITY,
325                    <$float>::MIN,
326                    -1.0,
327                    -<$float>::MIN_POSITIVE,
328                    -0.0,
329                    0.0,
330                    <$float>::MIN_POSITIVE,
331                    <$float>::from_bits(1),
332                    1.0,
333                    <$float>::MAX,
334                    <$float>::INFINITY,
335                    quiet_nan,
336                    neg_nan,
337                    payload_nan,
338                    max_nan,
339                ];
340                let nans = [quiet_nan, neg_nan, payload_nan, max_nan];
341
342                let generators: [&dyn Fn(&mut StdRng) -> $float; 5] = [
343                    &|r| r.random::<$float>() * 2.0 - 1.0,
344                    &|r| <$float>::from_bits(r.random()),
345                    &|r| *edges.choose(r).unwrap(),
346                    &|r| *nans.choose(r).unwrap(),
347                    &|r| {
348                        if r.random_bool(0.95) {
349                            quiet_nan
350                        } else {
351                            r.random()
352                        }
353                    },
354                ];
355
356                for generator in generators {
357                    for len in lengths() {
358                        let values: Vec<$float> = (0..len).map(|_| generator(&mut rng)).collect();
359                        let expected = fallback_min_max(&info, values.iter());
360                        match $kernel(&values) {
361                            Some(actual) => {
362                                // Compare bits to tell apart -0.0 / 0.0
363                                let to_bits = |(min, max, nans): ($float, $float, u64)| {
364                                    (min.to_bits(), max.to_bits(), nans)
365                                };
366                                assert_eq!(
367                                    to_bits(actual),
368                                    to_bits(expected.unwrap()),
369                                    "len {len}: {values:?}"
370                                );
371                            }
372                            // Only all NaN (or empty) input defers to the scalar path
373                            None => assert!(values.iter().all(|v| v.is_nan()), "{values:?}"),
374                        }
375                    }
376                }
377            }
378        };
379    }
380
381    float_tests!(test_min_max_f32, f32, u32, min_max_f32, PhysicalType::FLOAT);
382    float_tests!(
383        test_min_max_f64,
384        f64,
385        u64,
386        min_max_f64,
387        PhysicalType::DOUBLE
388    );
389}