slipstream/lib.rs
1#![doc(test(attr(deny(warnings))))]
2#![warn(missing_docs)]
3#![allow(non_camel_case_types)]
4#![cfg_attr(not(test), no_std)]
5
6//! This library helps writing code in a way that incentives the compiler to
7//! optimize the results better (without really doing anything itself).
8//!
9//! Modern compilers, including `rustc`, are able to come up with impressive ways to
10//! speed up the resulting code, using techniques like loop unrolling and
11//! autovectorization, routinely outperforming what one would hand-craft.
12//! Nevertheless, each optimisation has some assumptions that must be proven to hold
13//! before it can be applied.
14//!
15//! This library offers „vector“ types, like [`u16x8`], which act in a very similar
16//! way as little fixed-sized arrays (in this case it would be `[u16; 8]`), but with
17//! arithmetics defined for them. They also enforce alignment of the whole vectors.
18//! Therefore, one can write the algorithm in a way that works on these groups of
19//! data and make it easier for the compiler to prove the assumptions. This can
20//! result in multiple factor speed ups by giving the compiler these proofs „for
21//! free“ and allowing it to apply aggressive optimizations.
22//!
23//! Unlike several other SIMD libraries, this one doesn't do any actual explicit SIMD. That results
24//! in relatively simpler interface while still working on stable compiler. It also works in no-std
25//! environment. However, the optimisations are not guaranteed. In particular, while the crate may
26//! allow for a significant speed-ups, it can *also make your code slower*. When using the crate,
27//! you're strongly advised to benchmark.
28//!
29//! # Anatomy of the crate
30//!
31//! ## Vector types
32//!
33//! On the surface, there are types like [`u16x8`], which is just an wrapper around `[u16; 8]`.
34//! These wrappers act a bit like arrays (they can be dereferenced to a slice, they can be indexed)
35//! and have **common arithmetic traits** implemented. The arithmetic is applied to each index
36//! separately, eg:
37//!
38//! ```
39//! # use slipstream::prelude::*;
40//! let a = u8x2::new([1, 2]);
41//! let b = u8x2::new([3, 4]);
42//! assert_eq!(a + b, u8x2::new([4, 6]));
43//! ```
44//!
45//! All these types are backed by the generic [`Vector`] type. See the
46//! methods there to see how they can be created and how they interact.
47//!
48//! All these can be imported by importing prelude:
49//!
50//! ```
51//! # #[allow(unused_imports)]
52//! use slipstream::prelude::*;
53//! ```
54//!
55//! The names are based on primitive types, therefore there are types like [`u8x2`], [`i8x2`],
56//! [`f32x4`], [`f64x2`].
57//!
58//! There are some more types:
59//!
60//! * [`wu8x2`] is based on [`Wrapping<u8>`][core::num::Wrapping], [`wi8x2`] is based on
61//! [`Wrapping<i8>`][core::num::Wrapping].
62//! * [`bx2`] are vectors of [`bool`]s.
63//! * [`m8x2`] are mask vectors. They act *a bit* like booleans, but they have width and use all
64//! bits set to `1` for `true`. These can be used to [`blend`][Vector::blend] vectors together,
65//! mask loads and stores and are results of comparisons. The representation is inspired by what
66//! the vector instructions actually use, so they should be possible for the compiler to
67//! autovectorize. The widths match the types they work with ‒ comparing two [`u32x2`]s will
68//! result in [`m32x2`]. The lanes can be converted to/from [`bool`] with methods on the [`Mask`]
69//! trait, but usually these are just fed back to some other vector operations.
70//!
71//! ## Vectorization of slices
72//!
73//! While it might be better for performance to store all data already in the vector types, it
74//! oftentimes happen that the input is in form of a slice or multiple slices of the primitive
75//! types. It would be possible to chunk the input and load them into the vectors one at a time,
76//! either manually or by using something like the [`chunks_exact`][core::slice::ChunksExact]
77//! and [`zip`][core::iter::Iterator::zip]. Nevertheless, it turns out to be inconvenient and often
78//! too complex for the compiler to make sense of and vectorize properly.
79//!
80//! Therefore, the crate provides its own means for splitting the data into vectors, using the
81//! [`Vectorizable`] trait. This is implemented on const and mutable slices as well as tuples and
82//! small (fixed-sized) arrays of these. The trait adds the [`vectorize`][Vectorizable::vectorize]
83//! and [`vectorize_pad`][Vectorizable::vectorize_pad] methods.
84//!
85//! As the methods can't know into how wide vectors the input should be split, it is often needed
86//! to provide a type hint somewhere.
87//!
88//! ```rust
89//! # use slipstream::prelude::*;
90//! fn dot_product(l: &[f32], r: &[f32]) -> f32 {
91//! let mut result = f32x8::default();
92//! // This assumes l and r are of the same length and divisible by 8
93//! for (l, r) in (l, r).vectorize() {
94//! // Force the exact type of l and r vectors
95//! let (l, r): (f32x8, f32x8) = (l, r);
96//! result += l * r;
97//! }
98//! // Sum the 8 lanes together
99//! result.horizontal_sum()
100//! }
101//! # dot_product(&[], &[]);
102//! ```
103//!
104//! # Multiversioning and dynamic instruction set selection
105//!
106//! If used as in the examples above, the compiler chooses an instruction set at compile time,
107//! based on the command line arguments. By default these are conservative, to run on arbitrary
108//! (old) CPU. It is possible to either enable newer instructions at compile time (at the cost of
109//! not being able to run the program on the older CPUs) or compile multiple versions of the same
110//! function and choose the right one at runtime, depending on what the CPU actually supports.
111//!
112//! While this library doesn't provide any direct support for multiversioning, it has been observed
113//! to work reasonably well in combination with the [`multiversion`] crate.
114//!
115//! Note that using a newer and richer instruction set is not always a win. In some cases it can
116//! even lead to performance degradation. In particular:
117//!
118//! * Wide enough vectors must be used to take advantage of the 256 or more bits of the newer
119//! instruction set (using these with older instruction set is not a problem; the vector
120//! operations will simply translate to multiple narrower instructions). This might create larger
121//! „leftovers“ on the ends of slices that need to be handled in non-vectorized manner.
122//! * The CPU may need to switch state, possibly negotiate a higher power supply. This might lead
123//! to slow down before that happens and might degrade performance of neighboring cores.
124//! * Some AMD processors (Buldozers) know the instructions, but simulate them by dispatching the
125//! narrower instructions internally (at least it seems so, one 256bit instruction takes a bit
126//! longer than two 128bit ones).
127//!
128//! Depending on the workload, both slowdowns and full 2* speedups were observed. The chances of
129//! speedups are higher when there's a lot of data to crunch „in one go“ (so the CPU has time to
130//! „warm up“, the leftovers don't matter that much, etc).
131//!
132//! # Performance tuning tips
133//!
134//! The sole purpose of this library is to get faster programs, so here are few things to keep in
135//! mind when trying.
136//!
137//! This library (or SIMD in general) is not a silver bullet. It's good to tackle a lot of data
138//! crunching by sheer force (the hammer style approach), but can yield only multiplicative
139//! speedups (depending on the width of the instructions, on the size of the base type, etc, one
140//! can't expect more than 10 or 20 times speedup, usually less). Oftentimes, more high level
141//! optimizations bring significantly better results ‒ choosing a better algorithm, reordering the
142//! data in memory to avoid cache misses. These can give you orders of magnitude in some cases.
143//! Also, besides instruction level parallelism, one can try using threads to parallelize across
144//! cores (for example using [`rayon`]). Therefore, vectorization should be used in the latter
145//! stages of performance tuning.
146//!
147//! Also note that when used on a platform without any SIMD support, it can lead to both speed ups
148//! (due to loop unrolling) and slowdowns (probably due to exhaustion of available CPU registers).
149//!
150//! It is important to measure and profile. Not only because you want to spend the time optimizing
151//! the hot parts of the program which actually take significant amount of time, but because the
152//! autovectorizer and compiler optimizations sometimes produce surprising results.
153//!
154//! ## Performance characteristics
155//!
156//! In general, simple lane-wise operations are significantly faster than horizontal operations
157//! (when neighboring lanes may interact) and complex ones. Therefore, adding two vectors using the
158//! `+` operator is likely to end up being faster than the
159//! [`horizontal_sum`][Vector::horizontal_sum] or the [`gather_load`][Vector::gather_load]
160//! constructor.
161//!
162//! It is advisable to keep as much in vectors as possible instead of operating on separate lanes.
163//!
164//! Therefore, to compute a sum of bunch of numbers, split the input into vectors, sum these up and
165//! do single `horizontal_sum` at the very end.
166//!
167//! ```rust
168//! # use slipstream::prelude::*;
169//! fn sum(data: &[f32x8]) -> f32 {
170//! data
171//! .iter()
172//! .copied()
173//! .sum::<f32x8>() // Summing up whole f32x8 vectors, result is also f32x8
174//! .horizontal_sum() // Summing individual lanes of that vector
175//! }
176//! # assert_eq!(0.0, sum(&[]));
177//! ```
178//!
179//! Also keep in mind that there's usually some „warm up“ for vectorized part of code. This partly
180//! comes from the need to somehow deal with uneven ends (if the input is not divisible by the
181//! vector size). Also, some instructions require the CPU to switch state, possibly lower frequency
182//! and negotiate higher power supply, which may even hinder performance of neighboring cores (this
183//! is more of a problem for „newer“ instruction sets like AVX-512 than eg. SSE).
184//!
185//! Therefore, there's little advantage of interspersing otherwise non-vectorized code with
186//! occasional vector variable. The best results are for crunching big inputs all at once.
187//!
188//! ## Suggested process
189//!
190//! * Write the non-vectorized version first. Make sure to use the correct algorithm, avoid
191//! unnecessary work, etc.
192//! * Parallelize it across threads where it makes sense.
193//! * Prepare a micro-benchmark exercising the hot part.
194//! * Try rewriting it using the vector types in this crate, but keep the non-vectorized version
195//! around for comparison. Make sure to run the benchmark for both.
196//! * If the vectorized version doesn't meet the expectations (or even make things slower), you can
197//! check these things:
198//! - If using the [`multiversion`] crate, watch out for (not) inlining. The detected instruction
199//! set is not propagated to other functions called from the multiversioned one, only to the
200//! inlined ones.
201//! - Make sure to use reasonably sized vector type. On one side, it needs to be large enough to
202//! fill the whole SIMD register (128 bit for SSE and NEON, 256 for AVX, 512 bits for AVX-512).
203//! On the other side, it should not be too large ‒ while wider vectors can be simulated by
204//! executing multiple narrower instructions, they also take multiple registers and that may
205//! lead to unnecessary „juggling“.
206//! - See the profiler output if any particular part stands out. Oftentimes, some constructs like
207//! the [`zip`][core::iter::Iterator::zip] iterator adaptor were found to be problematic. If a
208//! construct is too complex for rustc to „see through“, it can be helped by rewriting that
209//! particular part manually in a simpler way. Pulling slice range checks before the loop might
210//! help too, as rustc no longer has to ensure a panic from the violation would happen at the
211//! right time in the middle of processing.
212//! - Check the assembler output if it looks sane. Seeing if it looks vectorized can be done
213//! without extensive assembler knowledge ‒ SIMD instructions have longer names and use
214//! different named registers (`xmm?` ones for SSE, `ymm?` ones for AVX).
215//!
216//! See if the profiler can be configured to show inlined functions instead of counting the whole
217//! runtime to the whole function. Some profilers can even show annotated assembler code,
218//! pinpointing the instruction or area that takes long time. In such case, be aware that an
219//! instruction might take a long time because it waits on a data dependency (some preceding
220//! instruction still being executed in the pipeline) or data from memory.
221//!
222//! For the `perf` profile, this can be done with `perf record --call-graph=dwarf <executable>`,
223//! `perf report` and `perf annotate`. Make sure to profile with both optimizations *and* debug
224//! symbols enabled (but if developing a proprietary thing, make sure to ship *without* the debug
225//! symbols).
226//!
227//! ```toml
228//! [profile.release]
229//! debug = 2
230//! ```
231//!
232//! When all else fails, you can always rewrite only parts of the algorithm using the explicit
233//! intrinsics in [`core::arch`] and leave the rest for autovectorizer. The vector types should be
234//! compatible for transmuting to the low-level vectors (eg. `__m128`).
235//!
236//! # Alternatives
237//!
238//! There are other crates that try to help with SIMD:
239//!
240//! * [`packed_simd`]: This is *the* official SIMD library. The downside is, this works only on
241//! nighty compiler and the timeline when this could get stabilized is unclear.
242//! * [`faster`]: Works only on nightly and looks abandoned.
243//! * [`simdeez`]: Doesn't have unsigned ints. Works on stable, but is unsound (can lead to UB
244//! without writing a single line of user `unsafe` code).
245//! * [`safe_simd`]: It has somewhat more complex API than this library, because it deals with
246//! instruction sets explicitly. It supports explicit vectorization (doesn't rely on
247//! autovectorizer). It is not yet released.
248//!
249//! [`multiversion`]: https://crates.io/crates/multiversion
250//! [`rayon`]: https://crates.io/crates/rayon
251//! [`packed_simd`]: https://crates.io/crates/packed_simd
252//! [`faster`]: https://crates.io/crates/faster
253//! [`simdeez`]: https://crates.io/crates/simdeez
254//! [`safe_simd`]: https://github.com/calebzulawski/safe_simd/
255
256pub mod iterators;
257pub mod mask;
258pub mod types;
259pub mod vector;
260
261pub use iterators::Vectorizable;
262pub use mask::Mask;
263pub use types::*;
264pub use vector::Vector;
265
266/// Commonly used imports
267///
268/// This can be imported to get all the vector types and all the relevant user-facing traits of the
269/// crate.
270pub mod prelude {
271 pub use crate::types::*;
272 pub use crate::vector::Masked as _;
273 pub use crate::Mask as _;
274 pub use crate::Vectorizable as _;
275}
276
277mod inner {
278 use core::num::Wrapping;
279
280 use crate::mask::{m128, m16, m32, m64, m8, msize, Mask};
281
282 /// A trait to enable vectors to use this type as the base type.
283 ///
284 /// # Safety
285 ///
286 /// This is in a private module to prevent users creating their own „crazy“ vector
287 /// implementations. We make some non-trivial assumptions about the inner types and be are
288 /// conservative at least until we figure out what *exact* assumptions these are and formalize
289 /// them.
290 pub unsafe trait Repr: Send + Sync + Copy + 'static {
291 type Mask: Mask;
292 const ONE: Self;
293 }
294
295 unsafe impl Repr for Wrapping<u8> {
296 type Mask = m8;
297 const ONE: Wrapping<u8> = Wrapping(1);
298 }
299 unsafe impl Repr for Wrapping<u16> {
300 type Mask = m16;
301 const ONE: Wrapping<u16> = Wrapping(1);
302 }
303 unsafe impl Repr for Wrapping<u32> {
304 type Mask = m32;
305 const ONE: Wrapping<u32> = Wrapping(1);
306 }
307 unsafe impl Repr for Wrapping<u64> {
308 type Mask = m64;
309 const ONE: Wrapping<u64> = Wrapping(1);
310 }
311 unsafe impl Repr for Wrapping<u128> {
312 type Mask = m128;
313 const ONE: Wrapping<u128> = Wrapping(1);
314 }
315 unsafe impl Repr for Wrapping<usize> {
316 type Mask = msize;
317 const ONE: Wrapping<usize> = Wrapping(1);
318 }
319 unsafe impl Repr for u8 {
320 type Mask = m8;
321 const ONE: u8 = 1;
322 }
323 unsafe impl Repr for u16 {
324 type Mask = m16;
325 const ONE: u16 = 1;
326 }
327 unsafe impl Repr for u32 {
328 type Mask = m32;
329 const ONE: u32 = 1;
330 }
331 unsafe impl Repr for u64 {
332 type Mask = m64;
333 const ONE: u64 = 1;
334 }
335 unsafe impl Repr for u128 {
336 type Mask = m128;
337 const ONE: u128 = 1;
338 }
339 unsafe impl Repr for usize {
340 type Mask = msize;
341 const ONE: usize = 1;
342 }
343
344 unsafe impl Repr for Wrapping<i8> {
345 type Mask = m8;
346 const ONE: Wrapping<i8> = Wrapping(1);
347 }
348 unsafe impl Repr for Wrapping<i16> {
349 type Mask = m16;
350 const ONE: Wrapping<i16> = Wrapping(1);
351 }
352 unsafe impl Repr for Wrapping<i32> {
353 type Mask = m32;
354 const ONE: Wrapping<i32> = Wrapping(1);
355 }
356 unsafe impl Repr for Wrapping<i64> {
357 type Mask = m64;
358 const ONE: Wrapping<i64> = Wrapping(1);
359 }
360 unsafe impl Repr for Wrapping<i128> {
361 type Mask = m128;
362 const ONE: Wrapping<i128> = Wrapping(1);
363 }
364 unsafe impl Repr for Wrapping<isize> {
365 type Mask = msize;
366 const ONE: Wrapping<isize> = Wrapping(1);
367 }
368 unsafe impl Repr for i8 {
369 type Mask = m8;
370 const ONE: i8 = 1;
371 }
372 unsafe impl Repr for i16 {
373 type Mask = m16;
374 const ONE: i16 = 1;
375 }
376 unsafe impl Repr for i32 {
377 type Mask = m32;
378 const ONE: i32 = 1;
379 }
380 unsafe impl Repr for i64 {
381 type Mask = m64;
382 const ONE: i64 = 1;
383 }
384 unsafe impl Repr for i128 {
385 type Mask = m128;
386 const ONE: i128 = 1;
387 }
388 unsafe impl Repr for isize {
389 type Mask = msize;
390 const ONE: isize = 1;
391 }
392
393 unsafe impl Repr for f32 {
394 type Mask = m32;
395 const ONE: f32 = 1.0;
396 }
397 unsafe impl Repr for f64 {
398 type Mask = m64;
399 const ONE: f64 = 1.0;
400 }
401 unsafe impl<M: Mask> Repr for M {
402 type Mask = Self;
403 const ONE: M = M::TRUE;
404 }
405}
406
407/// Free-standing version of [`Vectorizable::vectorize`].
408///
409/// This is the same as `a.vectorize()`. Nevertheless, this version might be more convenient as it
410/// allows hinting the result vector type with turbofish.
411///
412/// ```rust
413/// # use slipstream::prelude::*;
414/// let data = [1, 2, 3, 4];
415/// for v in slipstream::vectorize::<u32x2, _>(&data[..]) {
416/// println!("{:?}", v);
417/// }
418/// ```
419#[inline(always)]
420pub fn vectorize<V, A>(a: A) -> impl Iterator<Item = V>
421where
422 A: Vectorizable<V>,
423{
424 a.vectorize()
425}
426
427/// Free-standing version of [`Vectorizable::vectorize_pad`].
428///
429/// Equivalent to `a.vectorize_pad(pad)`, but may be more convenient or readable in certain cases.
430///
431/// ```rust
432/// # use slipstream::prelude::*;
433/// let data = [1, 2, 3, 4, 5, 6];
434/// let v = slipstream::vectorize_pad(&data[..], i32x4::splat(-1)).collect::<Vec<_>>();
435/// assert_eq!(v, vec![i32x4::new([1, 2, 3, 4]), i32x4::new([5, 6, -1, -1])]);
436/// ```
437#[inline(always)]
438pub fn vectorize_pad<V, A>(a: A, pad: A::Padding) -> impl Iterator<Item = V>
439where
440 A: Vectorizable<V>,
441{
442 a.vectorize_pad(pad)
443}
444
445#[cfg(test)]
446mod tests {
447 use crate::prelude::*;
448
449 #[test]
450 fn minmax() {
451 let a = u32x4::new([1, 4, 8, 9]);
452 let b = u32x4::new([3, 3, 5, 11]);
453
454 assert_eq!(a.minimum(b), u32x4::new([1, 3, 5, 9]));
455 assert_eq!(a.maximum(b), u32x4::new([3, 4, 8, 11]));
456 assert_eq!(a.minimum(b), b.minimum(a));
457 assert_eq!(a.maximum(b), b.maximum(a));
458 assert_eq!(a.maximum(b).ge(a.minimum(b)), m32x4::splat(m32::TRUE));
459 }
460}