ARROW-10002: [Rust] Remove trait specialization from arrow crate#8485
ARROW-10002: [Rust] Remove trait specialization from arrow crate#8485jorgecarleitao wants to merge 2 commits intoapache:masterfrom jorgecarleitao:simp_types
Conversation
|
fyi @andygrove @alamb |
| } | ||
| } | ||
|
|
||
| fn as_datetime<T: ArrowPrimitiveType>(v: i64) -> Option<NaiveDateTime> { |
There was a problem hiding this comment.
This places these functions outside the impl, so that we can use them:
When used with ArrowNumericType, it can convert itself to i64 and thus this is simple.
When we use it with PrimitiveType, we use to_usize to convert to i64.
| let buffer = MutableBuffer::new(capacity * mem::size_of::<T::Native>()); | ||
| #[inline] | ||
| fn new(capacity: usize) -> Self { | ||
| let buffer = if T::DATA_TYPE == DataType::Boolean { |
There was a problem hiding this comment.
Note that after templating, this will be either if a == a or if a == b, which I am hoping the compiler to optimize out. I placed an inline to help the compiler a bit, but I am not sure it is really needed.
There was a problem hiding this comment.
I think using an if for specialization rather than the compiler is totally legit
There was a problem hiding this comment.
Nice! I used a similar trick for the simd sum aggregation that the compiler was also able to optimize away (https://github.com/apache/arrow/pull/8370/files#diff-53a8522c4a482c9566b9032e1bd2c7990bab29640857cb40411beb7158189e75R625)
There was a problem hiding this comment.
I see this in profiling for the BufferBuilderTrait append function:
Which accounts for ~9% of the instruction fetches according to callgrind when running the TPHC 12 benchmark.
So it seems the if T::DATA_TYPE == DataType::Boolean is not being optimized out and goes into the partial eq implementation (or the profiler is wrong), maybe because the implementation there is not easy to optimize / specialize for this case. I will check if another implementation (pattern match?) solves this.
| return false; | ||
| } | ||
|
|
||
| if T::DATA_TYPE == DataType::Boolean { |
There was a problem hiding this comment.
Here we use the same principle: this will be a constant if a == a or if a == b.
| T: ArrowNumericType, | ||
| { | ||
| let array_type = T::get_data_type(); | ||
| let array_type = T::DATA_TYPE; |
There was a problem hiding this comment.
this could actually be removed by making filter_array_impl a generic over T. I left for a future exercise.
| //! | ||
| //! The parquet implementation is on a [separate crate](https://crates.io/crates/parquet) | ||
|
|
||
| #![feature(specialization)] |
There was a problem hiding this comment.
#arewestableyet? This is great, I've just compiled Arrow on stable Rust. I think it's just parquet that still uses it after this PR. If DataFusion doesn't use any nightly features, one could make parquet optional, and then Parquet will be the only one left.
alamb
left a comment
There was a problem hiding this comment.
This is a really nice change -- it makes sense to me and I (non bindingly) vote to
!
| write!(f, "PrimitiveArray<{:?}>\n[\n", T::DATA_TYPE)?; | ||
| print_long_array(self, f, |array, index, f| match T::DATA_TYPE { | ||
| DataType::Date32(_) | DataType::Date64(_) => { | ||
| match array.value_as_date(index) { |
There was a problem hiding this comment.
so the primary difference here is that all types get converted to i64 and then converted back to the appropriate (potentially smaller) native type by the code (and the compiler is presumably smart enough to make it all happen efficiently)
And by structuring the code in that way, we only need a single type parameter (i64->T rather than all combinations of input and output types?)
There was a problem hiding this comment.
The journey to usize via num-traits, then to i64, feels long, but I see the compiler optimises the function nicely in release mode.
pub extern fn convert_u32_to_i64(value: u32) -> i64 {
num::ToPrimitive::to_usize(&value).unwrap() as i64
}playground::convert_u32_to_i64:
movl %edi, %eax
retq| let buffer = MutableBuffer::new(capacity * mem::size_of::<T::Native>()); | ||
| #[inline] | ||
| fn new(capacity: usize) -> Self { | ||
| let buffer = if T::DATA_TYPE == DataType::Boolean { |
There was a problem hiding this comment.
I think using an if for specialization rather than the compiler is totally legit
|
For the sake of creating less friction at the user side, can you make get_data_type const fn with the same name rather than const with capitals? Still it will generate exactly the same code at the call site. |
I think this won't be possible as functions in traits can't be const |
|
@nevi-me true :/. We go to datatype from generics. const fn also accepts only |
There was a problem hiding this comment.
I've reviewed this and compiled it with nightly, thanks for this @jorgecarleitao. Arrow will be stable in the next release (if one doesn't use simd) 🎆
This PR removes trait specialization by leveraging the compiler to remove trivial `if` statements. I verified that the assembly code was the same in a [simple example](https://rust.godbolt.org/z/qrcW8W). I do not know if this generalizes to our use-case, but I suspect so as LLVM is (hopefully) removing trivial branches like `if a != a`. The change `get_data_type()` to `DATA_TYPE` is not necessary. I did it before realizing this. IMO it makes it more explicit that this is not a function, but a constant, but we can revert it. Closes #8485 from jorgecarleitao/simp_types Authored-by: Jorge C. Leitao <jorgecarleitao@gmail.com> Signed-off-by: Neville Dipale <nevilledips@gmail.com>

This PR removes trait specialization by leveraging the compiler to remove trivial
ifstatements.I verified that the assembly code was the same in a simple example. I do not know if this generalizes to our use-case, but I suspect so as LLVM is (hopefully) removing trivial branches like
if a != a.The change
get_data_type()toDATA_TYPEis not necessary. I did it before realizing this. IMO it makes it more explicit that this is not a function, but a constant, but we can revert it.