This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 2c50074ca7 perf(arrow-array): compile array `Debug` formatting helpers
once, not per primitive type (~2% binary size reduction) (#10890)
2c50074ca7 is described below
commit 2c50074ca7ec21c583e7b7b6b2d6933a02059d2c
Author: Andrew Lamb <[email protected]>
AuthorDate: Fri Aug 28 07:17:18 2026 -0400
perf(arrow-array): compile array `Debug` formatting helpers once, not per
primitive type (~2% binary size reduction) (#10890)
# Which issue does this PR close?
- Closes #10889.
# Rationale for this change
The `Debug` impl for `PrimitiveArray<T>` is monomorphized for all ~32
primitive types (aka 32 copies) but has a big match statement that
dispatches on the type and only a small part is genuinely type-specific;
everything else depends only on the runtime `DataType` and the value as
`i64`.
This means that we have 32 copies of everything (and we get 32 copies of
that same code in each downstream tratt)
# What changes are included in this PR?
1. Update the code so there is one copy rather than a bunch
## Measurements
I measured the size of the `cast_kernels` binary with
```shell
cargo bench --bench cast_kernels --no-run
```
To see the actual number of bytes saved (and it was substantial -- 225K
/ 1.7%:
| before | after | delta |
|---:|---:|---:|
| 13,213,184 bytes | 12,988,032 bytes | **−225,152 bytes (−1.7%)** |
Measured on the `cast_kernels` bench target with
```shell
cargo llvm-lines --release -p arrow --features test_utils --bench
cast_kernels
```
| | before | after | delta |
|---|---:|---:|---:|
| total IR lines for the bench target | 323,255 | 285,694 | **−37,561
(−11.6%)** |
| `<PrimitiveArray<T> as Debug>::fmt` + closures | 36,851 lines / 42
inst. | 4,737 lines / 56 inst. | −87% |
| `print_long_array` | 14,730 lines / 16 inst. | 0 (compiled once in
`arrow-array`) | −100% |
| `temporal_conversions::*` pulled in by `Debug` | 4,299 lines / 46
inst. | 0 (compiled once in `arrow-array`) | −100% |
`Debug` formatting is not performance-sensitive, so this should be a
pure code-size / compile-time win.
# Are these changes tested?
Covered by the existing `Debug` tests (`test_primitive_array_debug`, the
timestamp-with/without-timezone and invalid-timezone tests, etc.), which
assert the exact formatted output and pass unchanged.
# Are there any user-facing changes?
No. `print_long_array` is a private helper and the new
`*_with_data_type` conversion functions are `pub(crate)`. `Debug` output
is unchanged.
---
arrow-array/src/array/boolean_array.rs | 4 +-
arrow-array/src/array/byte_array.rs | 4 +-
arrow-array/src/array/byte_view_array.rs | 4 +-
arrow-array/src/array/fixed_size_binary_array.rs | 4 +-
arrow-array/src/array/fixed_size_list_array.rs | 4 +-
arrow-array/src/array/list_array.rs | 4 +-
arrow-array/src/array/list_view_array.rs | 4 +-
arrow-array/src/array/map_array.rs | 4 +-
arrow-array/src/array/mod.rs | 19 +--
arrow-array/src/array/primitive_array.rs | 142 +++++++++++++----------
arrow-array/src/array/struct_array.rs | 2 +-
arrow-array/src/temporal_conversions.rs | 41 ++++++-
12 files changed, 146 insertions(+), 90 deletions(-)
diff --git a/arrow-array/src/array/boolean_array.rs
b/arrow-array/src/array/boolean_array.rs
index 0e5df12825..be80bc553a 100644
--- a/arrow-array/src/array/boolean_array.rs
+++ b/arrow-array/src/array/boolean_array.rs
@@ -73,8 +73,8 @@ pub struct BooleanArray {
impl std::fmt::Debug for BooleanArray {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "BooleanArray\n[\n")?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/byte_array.rs
b/arrow-array/src/array/byte_array.rs
index be4ecdee50..728aeb342a 100644
--- a/arrow-array/src/array/byte_array.rs
+++ b/arrow-array/src/array/byte_array.rs
@@ -467,8 +467,8 @@ impl<T: ByteArrayType> GenericByteArray<T> {
impl<T: ByteArrayType> std::fmt::Debug for GenericByteArray<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}{}Array\n[\n", T::Offset::PREFIX, T::PREFIX)?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/byte_view_array.rs
b/arrow-array/src/array/byte_view_array.rs
index 7a7e94d111..0ff1a77c49 100644
--- a/arrow-array/src/array/byte_view_array.rs
+++ b/arrow-array/src/array/byte_view_array.rs
@@ -878,8 +878,8 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
impl<T: ByteViewType + ?Sized> Debug for GenericByteViewArray<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}ViewArray\n[\n", T::PREFIX)?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/fixed_size_binary_array.rs
b/arrow-array/src/array/fixed_size_binary_array.rs
index 53c8cdb8fd..fce539c614 100644
--- a/arrow-array/src/array/fixed_size_binary_array.rs
+++ b/arrow-array/src/array/fixed_size_binary_array.rs
@@ -812,8 +812,8 @@ impl<const N: usize> TryFrom<Vec<&[u8; N]>> for
FixedSizeBinaryArray {
impl std::fmt::Debug for FixedSizeBinaryArray {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "FixedSizeBinaryArray<{}>\n[\n", self.value_length())?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/fixed_size_list_array.rs
b/arrow-array/src/array/fixed_size_list_array.rs
index 901686a4fc..de7b97c231 100644
--- a/arrow-array/src/array/fixed_size_list_array.rs
+++ b/arrow-array/src/array/fixed_size_list_array.rs
@@ -609,8 +609,8 @@ impl ArrayAccessor for FixedSizeListArray {
impl std::fmt::Debug for FixedSizeListArray {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "FixedSizeListArray<{}>\n[\n", self.value_length())?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/list_array.rs
b/arrow-array/src/array/list_array.rs
index bd08a6ccc4..ccab56db99 100644
--- a/arrow-array/src/array/list_array.rs
+++ b/arrow-array/src/array/list_array.rs
@@ -706,8 +706,8 @@ impl<OffsetSize: OffsetSizeTrait> std::fmt::Debug for
GenericListArray<OffsetSiz
let prefix = OffsetSize::PREFIX;
write!(f, "{prefix}ListArray\n[\n")?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/list_view_array.rs
b/arrow-array/src/array/list_view_array.rs
index 9a44fcbd6e..1983e1a841 100644
--- a/arrow-array/src/array/list_view_array.rs
+++ b/arrow-array/src/array/list_view_array.rs
@@ -568,8 +568,8 @@ impl<OffsetSize: OffsetSizeTrait> std::fmt::Debug for
GenericListViewArray<Offse
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let prefix = OffsetSize::PREFIX;
write!(f, "{prefix}ListViewArray\n[\n")?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/map_array.rs
b/arrow-array/src/array/map_array.rs
index 9c7207210a..9c7996cab3 100644
--- a/arrow-array/src/array/map_array.rs
+++ b/arrow-array/src/array/map_array.rs
@@ -617,8 +617,8 @@ impl ArrayAccessor for &MapArray {
impl std::fmt::Debug for MapArray {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MapArray\n[\n")?;
- print_long_array(self, f, |array, index, f| {
- std::fmt::Debug::fmt(&array.value(index), f)
+ print_long_array(self, f, &mut |index, f| {
+ std::fmt::Debug::fmt(&self.value(index), f)
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/mod.rs b/arrow-array/src/array/mod.rs
index b3eb5eeaf2..6379b1f341 100644
--- a/arrow-array/src/array/mod.rs
+++ b/arrow-array/src/array/mod.rs
@@ -1060,11 +1060,16 @@ unsafe fn get_offsets_from_buffer<O: ArrowNativeType>(
}
/// Helper function for printing potentially long arrays.
-fn print_long_array<A, F>(array: &A, f: &mut std::fmt::Formatter, print_item:
F) -> std::fmt::Result
-where
- A: Array,
- F: Fn(&A, usize, &mut std::fmt::Formatter) -> std::fmt::Result,
-{
+///
+/// Note this function is deliberately not generic over the array or closure
+/// type: it is called from `Debug` impls that are instantiated for many
+/// concrete array types, and using dynamic dispatch here avoids duplicating
+/// this function's code in each of those instantiations
+fn print_long_array(
+ array: &dyn Array,
+ f: &mut std::fmt::Formatter,
+ print_item: &mut dyn FnMut(usize, &mut std::fmt::Formatter) ->
std::fmt::Result,
+) -> std::fmt::Result {
let head = std::cmp::min(10, array.len());
for i in 0..head {
@@ -1072,7 +1077,7 @@ where
writeln!(f, " null,")?;
} else {
write!(f, " ")?;
- print_item(array, i, f)?;
+ print_item(i, f)?;
writeln!(f, ",")?;
}
}
@@ -1088,7 +1093,7 @@ where
writeln!(f, " null,")?;
} else {
write!(f, " ")?;
- print_item(array, i, f)?;
+ print_item(i, f)?;
writeln!(f, ",")?;
}
}
diff --git a/arrow-array/src/array/primitive_array.rs
b/arrow-array/src/array/primitive_array.rs
index 09ab889f73..c0d000e7b7 100644
--- a/arrow-array/src/array/primitive_array.rs
+++ b/arrow-array/src/array/primitive_array.rs
@@ -19,7 +19,8 @@ use crate::array::print_long_array;
use crate::builder::{BooleanBufferBuilder, PrimitiveBuilder};
use crate::iterator::PrimitiveIter;
use crate::temporal_conversions::{
- as_date, as_datetime, as_datetime_with_timezone, as_duration, as_time,
+ as_datetime, as_datetime_with_data_type, as_datetime_with_timezone,
+ as_datetime_with_timezone_and_data_type, as_duration, as_time,
as_time_with_data_type,
};
use crate::timezone::Tz;
use crate::trusted_len::trusted_len_unzip;
@@ -1370,73 +1371,90 @@ where
}
}
-impl<T: ArrowPrimitiveType> std::fmt::Debug for PrimitiveArray<T> {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- let data_type = self.data_type();
-
- write!(f, "PrimitiveArray<{data_type}>\n[\n")?;
- print_long_array(self, f, |array, index, f| match data_type {
- DataType::Date32 | DataType::Date64 => {
- let v = self.value(index).to_i64().unwrap();
- match as_date::<T>(v) {
- Some(date) => write!(f, "{date:?}"),
- None => {
- write!(
- f,
- "Cast error: Failed to convert {v} to temporal for
{data_type}"
- )
- }
+/// Writes the `Debug` representation of a single temporal value (converted to
+/// `i64`) of the given [`DataType`] to `f`
+fn write_temporal_value(
+ f: &mut std::fmt::Formatter,
+ data_type: &DataType,
+ v: i64,
+) -> std::fmt::Result {
+ match data_type {
+ DataType::Date32 | DataType::Date64 => {
+ match as_datetime_with_data_type(data_type, v).map(|datetime|
datetime.date()) {
+ Some(date) => write!(f, "{date:?}"),
+ None => {
+ write!(
+ f,
+ "Cast error: Failed to convert {v} to temporal for
{data_type}"
+ )
}
}
- DataType::Time32(_) | DataType::Time64(_) => {
- let v = self.value(index).to_i64().unwrap();
- match as_time::<T>(v) {
- Some(time) => write!(f, "{time:?}"),
- None => {
- write!(
- f,
- "Cast error: Failed to convert {v} to temporal for
{data_type}"
- )
- }
- }
+ }
+ DataType::Time32(_) | DataType::Time64(_) => match
as_time_with_data_type(data_type, v) {
+ Some(time) => write!(f, "{time:?}"),
+ None => {
+ write!(
+ f,
+ "Cast error: Failed to convert {v} to temporal for
{data_type}"
+ )
}
- DataType::Timestamp(_, tz_string_opt) => {
- let v = self.value(index).to_i64().unwrap();
- match tz_string_opt {
- // for Timestamp with TimeZone
- Some(tz_string) => {
- match tz_string.parse::<Tz>() {
- // if the time zone is valid, construct a
DateTime<Tz> and format it as rfc3339
- Ok(tz) => match as_datetime_with_timezone::<T>(v,
tz) {
- Some(datetime) => write!(f, "{}",
datetime.to_rfc3339()),
- None => write!(
- f,
- "Cast error: Failed to convert {v} to
timestamp for {data_type}"
- ),
- },
- // if the time zone is invalid, shows
NaiveDateTime with an error message
- Err(_) => match as_datetime::<T>(v) {
- Some(datetime) => {
- write!(f, "{datetime:?} (Unknown Time Zone
'{tz_string}')")
- }
- None => write!(
- f,
- "Cast error: Failed to convert {v} to
timestamp for {data_type}"
- ),
- },
- }
+ },
+ DataType::Timestamp(_, tz_string_opt) => {
+ match tz_string_opt {
+ // for Timestamp with TimeZone
+ Some(tz_string) => {
+ match tz_string.parse::<Tz>() {
+ // if the time zone is valid, construct a DateTime<Tz>
and format it as rfc3339
+ Ok(tz) => match
as_datetime_with_timezone_and_data_type(data_type, v, tz) {
+ Some(datetime) => write!(f, "{}",
datetime.to_rfc3339()),
+ None => write!(
+ f,
+ "Cast error: Failed to convert {v} to
timestamp for {data_type}"
+ ),
+ },
+ // if the time zone is invalid, shows NaiveDateTime
with an error message
+ Err(_) => match as_datetime_with_data_type(data_type,
v) {
+ Some(datetime) => {
+ write!(f, "{datetime:?} (Unknown Time Zone
'{tz_string}')")
+ }
+ None => write!(
+ f,
+ "Cast error: Failed to convert {v} to
timestamp for {data_type}"
+ ),
+ },
}
- // for Timestamp without TimeZone
- None => match as_datetime::<T>(v) {
- Some(datetime) => write!(f, "{datetime:?}"),
- None => write!(
- f,
- "Cast error: Failed to convert {v} to timestamp
for {data_type}"
- ),
- },
}
+ // for Timestamp without TimeZone
+ None => match as_datetime_with_data_type(data_type, v) {
+ Some(datetime) => write!(f, "{datetime:?}"),
+ None => write!(
+ f,
+ "Cast error: Failed to convert {v} to timestamp for
{data_type}"
+ ),
+ },
+ }
+ }
+ _ => unreachable!("write_temporal_value called with non-temporal type
{data_type}"),
+ }
+}
+
+impl<T: ArrowPrimitiveType> std::fmt::Debug for PrimitiveArray<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let data_type = self.data_type();
+
+ write!(f, "PrimitiveArray<{data_type}>\n[\n")?;
+ // Keep the per-value closure as small as possible: temporal formatting
+ // is dispatched to the non-generic `write_temporal_value` so it is not
+ // instantiated for every primitive type (see #10889)
+ print_long_array(self, f, &mut |index, f| match data_type {
+ DataType::Date32
+ | DataType::Date64
+ | DataType::Time32(_)
+ | DataType::Time64(_)
+ | DataType::Timestamp(_, _) => {
+ write_temporal_value(f, data_type,
self.value(index).to_i64().unwrap())
}
- _ => std::fmt::Debug::fmt(&array.value(index), f),
+ _ => std::fmt::Debug::fmt(&self.value(index), f),
})?;
write!(f, "]")
}
diff --git a/arrow-array/src/array/struct_array.rs
b/arrow-array/src/array/struct_array.rs
index 767cb94fed..fa2718c7fd 100644
--- a/arrow-array/src/array/struct_array.rs
+++ b/arrow-array/src/array/struct_array.rs
@@ -571,7 +571,7 @@ impl std::fmt::Debug for StructArray {
writeln!(f, "StructArray")?;
writeln!(f, "-- validity:")?;
writeln!(f, "[")?;
- print_long_array(self, f, |_array, _index, f| write!(f, "valid"))?;
+ print_long_array(self, f, &mut |_index, f| write!(f, "valid"))?;
writeln!(f, "]\n[")?;
for (child_index, name) in self.column_names().iter().enumerate() {
let column = self.column(child_index);
diff --git a/arrow-array/src/temporal_conversions.rs
b/arrow-array/src/temporal_conversions.rs
index 375c19bc87..0197acdc51 100644
--- a/arrow-array/src/temporal_conversions.rs
+++ b/arrow-array/src/temporal_conversions.rs
@@ -241,7 +241,17 @@ pub fn duration_ns_to_duration(v: i64) -> Duration {
/// Converts an [`ArrowPrimitiveType`] to [`NaiveDateTime`]
pub fn as_datetime<T: ArrowPrimitiveType>(v: i64) -> Option<NaiveDateTime> {
- match T::DATA_TYPE {
+ as_datetime_with_data_type(&T::DATA_TYPE, v)
+}
+
+/// Converts a value of the given [`DataType`] to [`NaiveDateTime`]
+///
+/// Non-generic counterpart of [`as_datetime`], driven by the runtime
+/// [`DataType`] so callers that already dispatch on the data type (e.g.
`Debug`
+/// impls) do not monomorphize this logic for every primitive type
+#[inline]
+pub(crate) fn as_datetime_with_data_type(data_type: &DataType, v: i64) ->
Option<NaiveDateTime> {
+ match data_type {
DataType::Date32 => date32_to_datetime(v as i32),
DataType::Date64 => date64_to_datetime(v),
DataType::Time32(_) | DataType::Time64(_) => None,
@@ -259,7 +269,20 @@ pub fn as_datetime<T: ArrowPrimitiveType>(v: i64) ->
Option<NaiveDateTime> {
/// Converts an [`ArrowPrimitiveType`] to [`DateTime<Tz>`]
pub fn as_datetime_with_timezone<T: ArrowPrimitiveType>(v: i64, tz: Tz) ->
Option<DateTime<Tz>> {
- let naive = as_datetime::<T>(v)?;
+ as_datetime_with_timezone_and_data_type(&T::DATA_TYPE, v, tz)
+}
+
+/// Converts a value of the given [`DataType`] to [`DateTime<Tz>`]
+///
+/// Non-generic counterpart of [`as_datetime_with_timezone`], see
+/// [`as_datetime_with_data_type`]
+#[inline]
+pub(crate) fn as_datetime_with_timezone_and_data_type(
+ data_type: &DataType,
+ v: i64,
+ tz: Tz,
+) -> Option<DateTime<Tz>> {
+ let naive = as_datetime_with_data_type(data_type, v)?;
Some(Utc.from_utc_datetime(&naive).with_timezone(&tz))
}
@@ -270,7 +293,15 @@ pub fn as_date<T: ArrowPrimitiveType>(v: i64) ->
Option<NaiveDate> {
/// Converts an [`ArrowPrimitiveType`] to [`NaiveTime`]
pub fn as_time<T: ArrowPrimitiveType>(v: i64) -> Option<NaiveTime> {
- match T::DATA_TYPE {
+ as_time_with_data_type(&T::DATA_TYPE, v)
+}
+
+/// Converts a value of the given [`DataType`] to [`NaiveTime`]
+///
+/// Non-generic counterpart of [`as_time`], see [`as_datetime_with_data_type`]
+#[inline]
+pub(crate) fn as_time_with_data_type(data_type: &DataType, v: i64) ->
Option<NaiveTime> {
+ match data_type {
DataType::Time32(unit) => {
// safe to immediately cast to u32 as `self.value(i)` is positive
i32
let v = v as u32;
@@ -285,7 +316,9 @@ pub fn as_time<T: ArrowPrimitiveType>(v: i64) ->
Option<NaiveTime> {
TimeUnit::Nanosecond => time64ns_to_time(v),
_ => None,
},
- DataType::Timestamp(_, _) => as_datetime::<T>(v).map(|datetime|
datetime.time()),
+ DataType::Timestamp(_, _) => {
+ as_datetime_with_data_type(data_type, v).map(|datetime|
datetime.time())
+ }
DataType::Date32 | DataType::Date64 => NaiveTime::from_hms_opt(0, 0,
0),
DataType::Interval(_) => None,
_ => None,