Add f64<->f32 widening/narrowing conversions - #127
Conversation
950c4db to
d217ef5
Compare
Ralith
left a comment
There was a problem hiding this comment.
Any idea how to expose this to native-width code?
|
Probably through adding |
|
What would the return types be, though? We only have associated types of one width. |
|
Hm, I see the issue. Maybe the Zulip thread's "split when widening / combine when narrowing" approach is better for this? |
|
I think that's probably the best we can do in current Rust, yeah. No reason not to leave that for follow-up work, of course. |
|
If the split/combine approach is what we're going with, it might be good to separate the conversions from the existing widen/narrow ops' convention. Like, let's say we want to implement the "generic" widen operation on @LaurenzV, would it be a good idea to just update the "widen/narrow" signatures to return and consume two vectors respectively? The |
|
Seems reasonable, but maybe something for office hours. |
We only had widen/narrow between u8 and u16 before. This PR removes that and builds a generic widen/narrow from the ground up, using the API from linebender#127. It covers all integer and float types. Implementations for floats, and for integers on AVX-512, NEON and WASM are a breeze. Very straightforward, love them. SSE4.2 and AVX2 only have saturating conversions but not truncating ones for integers, so truncating ones have to be emulated on top of saturating ones. There are also no conversions to/from 64-bit integers, so those have to be emulated too. I included saturating conversions in addition to truncating ones in the API, since all hardware has them natively, even older x86. This did end up adding a bit of complexity because 64-bit saturating casts have to be emulated on AVX2 and earlier. I think it's still worth keeping because they don't add that much. All the other complexity (truncating conversion emulation on AVX2 and earlier) is unavoidable. I've axed SSE2-specific kernels since autovectorization works okay for them, and I'm not willing to complicate this even further. Since this is still quite a lot of complexity due to all the x86 emulation, I've added extensive tests on concrete values as well as a random test under `#[ignore]` to be run in release mode used to validate it, same as for other non-trivial ops like linebender#276 Performance-wise rust-lang/rust#159464 bites us here (regression in Rust 1.96, 1.95 and earlier work fine) but it's not so bad that scalar wins, so nothing we can do about it right now (other than inline assembly but i'm not willing to go there). This is [expected to be fixed](rust-lang/rust#159464 (comment)) in the upgrade to LLVM 23 which is coming Soon™. In the meantime this PR is tuned for 1.95 on llvm-mca. ## Generic API considerations There's an API decision on how to expose these ops to generic code, specifically the int/float distinction. It's a tension between flexibility and brevity. In this PR I implemented both regular and saturating for floats and they just do the usual rounding, same as `as` casts. This abstracts over vector type if all you want is widen/narrow. However, if you want to operate on the widened vector afterwards, you need an additional generic bound, e.g. `V::Widened: SimdInt<S>`: <details><summary>Snippet with an additional bound</summary> <p> ```rust use fearless_simd::{SimdFrom, prelude::*, u8x16}; #[inline(always)] fn add_widened_halves<S, V>(value: V) -> V::Widened where S: Simd, V: SimdInt<S> + SimdWiden<S>, V::Widened: SimdInt<S>, { let (low, high) = value.widen(); low + high } #[inline(always)] fn fixed_width<S: Simd>(simd: S) -> [u16; 8] { let input = u8x16::simd_from( simd, [1, 2, 3, 4, 5, 6, 7, 8, 10, 20, 30, 40, 50, 60, 70, 80], ); add_widened_halves(input).into() } #[inline(always)] fn native_width<S: Simd>(simd: S) -> u16 { let input = S::u8s::from_fn(simd, |i| i as u8 + 1); add_widened_halves(input)[0] } ``` </p> </details> This is not a problem on one function, but this extra bound is infectious, so all callers have to require it too until a concrete type is reached. So you have `SimdWiden<S> + V::Widened: SimdInt<S>` propagated all the way up the call stack. Alternatively, one could argue floats really don't have the saturating variant and maybe they should only implement `narrow` but not `saturating_narrow`. So we could make two traits, `SimdNarrowInt<S>` and `SimdNarrowFloat<S>`. So instead of sometimes carrying the `SimdNarrow<S> + V::Narrowed: SimdInt<S>` bound your entire call stack you only carry `SimdNarrowInt<S>`, one having `saturating_*` and the other not, at the expense of not being able to abstract over ints and floats in widen/narrow ops anymore. Abstracting over ints and floats isn't that useful right now since `SimdBase` doesn't have anything on it, but this is something we can and probably should change now that masks aren't `SimdBase`, see linebender#301. Or we could have both `SimdWiden` and `SimdWidenInt`/`SimdWidenFloat`, with the current behavior and `SimdIntWiden`/`SimdFloatWiden` being shorthands to avoid carrying extra bounds - at the expense of more than one trait providing `narrow` and `widen` for each op, and that being potentially confusing. Part of linebender#297 Supersedes linebender#127
|
Superseded by #300 which is now merged, closing. |
These were requested in #simd > Quickly deinterleaving the underlying Simd arrays?, and seem useful.
They are a bit unusual in Neon, which has specific instructions for converting the low and high halves (and combining the two).
Due to some rearrangement and cleanup of the code generation, the reinterpret ops have been moved around a bit. They haven't been changed at all.