Skip to content

Commit c97e141

Browse files
committed
assembly godbolt in README.md
1 parent 0711e11 commit c97e141

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed

crates/core_simd/examples/README.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
### `stdsimd` examples
2+
3+
This crate is a port of example uses of `stdsimd`, mostly taken from the `packed_simd` crate.
4+
5+
The examples contain, as in the case of `dot_product.rs`, multiple ways of solving the problem, in order to show idiomatic uses of SIMD and iteration of performance designs.
6+
7+
Run the tests with the command
8+
9+
```
10+
cargo run --example dot_product
11+
```
12+
13+
and verify the code for `dot_product.rs` on your machine.
14+
15+
### `dot_product.rs`
16+
17+
This example code takes the dot product of two vectors. You are supposed to mulitply each pair of elements and add them all together.
18+
19+
The easiest way to inspect the assembly of the `scalar` code versions (the non-SIMD versions) is to [click this link](https://rust.godbolt.org/z/xM9Mxb14n) for a *mise en place* of what is going on.
20+
21+
#### Scalar versions of `dot_product`
22+
23+
What are we looking at? We have code snippets for calculating the dot product on opposite ends of the screen (works best on a computer and not mobile screen). In the middle panels, we have their output assembly, respectively. In the bottom oposite corners, we've setup `llvm-mca`, which stands for the LLVM Machine Code Analyzer, a workbench for measuring hardware performance statistics. We will need many such tools to get the most out of understanding and squeezing performance out of SIMD codes, so it's healthy to enter a "poke all the tools" frame of mind.
24+
25+
We can observe a couple of interesting things
26+
1. Both codes output almost identical assembly if the `opt-level` is set to `3`. What happens if you set them to `0,1,2`? Do they always give the same stats?
27+
2. Try disabling the `assert!` comment. What happens to the assembly? Why do you think that people avoid `panic!` in tight Rust code?
28+
3. You can target different instruction sets that have shorter/wider SIMD lanes. Trying adding `-target-feature=+avx2` in the compilation options (next to the `opt-level` flags) and see how the assembly changes.
29+
4. Just because `xmm` registers are being used, doesn't necessarily mean you're getting SIMD speedups. This is in part due to technical debt and backwards compatibility that `xmm` registers are easier to pass floats around in, but to really get them to operate on multiple data we need more direct control that what we are currently using.
30+
5. TODO In order to maximize throughput of our tight loop, we are looking to maximize/minimize these following measurements: throughput, ipc/cycle, etc.
31+
32+
-----
33+
34+
#### SIMD version of `dot_product`
35+
36+
Open up [this link in your browser of choice](https://rust.godbolt.org/z/85neY7Kcn). For the code on the left, the full suite of optimizations kick in when we get to `opt-level=2`, and we get ~50 lines of assembly total. When starting out, more compact assembly can be a decent indication of more streamlined coding, but it quickly dies out as a heuristic, so don't get too distracted by that factor. Looking at the `llvm-mca` window on the bottom left, we observe we get an (TODO IPC/throughput) of XXX, whereas in the previous example, our best code could only get to YYY. A couple of notes on this snippet:
37+
38+
```rust
39+
#![feature(portable_simd)]
40+
#![feature(array_chunks)]
41+
use std::simd::*;
42+
43+
// Other options to try instead of "avx2":
44+
// "sse"
45+
// "sse4.1"
46+
//#[target_feature(enable = "avx2")]
47+
pub unsafe fn dot_prod_simd_0(a: &[f32], b: &[f32]) -> f32 {
48+
// TODO handle remainder when a.len() % 4 != 0
49+
a.array_chunks::<4>()
50+
.map(|&a| f32x4::from_array(a))
51+
.zip(b.array_chunks::<4>().map(|&b| f32x4::from_array(b)))
52+
.map(|(a, b)| (a * b).reduce_sum())
53+
.sum()
54+
55+
```
56+
57+
1. SIMD comes in many flavors (instructions sets). These (like `sse`, `sse4.1`, `avx2`) describe the hardware capabilities of your current CPU. That is, if you don't have `avx512`, you physically do not have a SIMD vector that can hold 512 bytes at a time at most on your CPU.
58+
2. You can switch between different instruction sets by changing the `#![target-feature(...)]` macro above the function, as well as declaring it unsafe.
59+
3. Inside Godbolt, you can hover over an instruction to display a tooltip of what it says. Try hovering your mouse over `mulps` and reading what it says.
60+
61+
We need to find a way to reduce the amount of *data movement*. We're not doing enough work for all the moving floats into and out of the `xmm` registers. This isn't surprising if we stop and try to look at the code for a bit: `dot_prod_simd_0` is loading 4 floats into `xmm` `a`, then the corresponding 4 floats from `b`, multiplying them (the efficient part), and then doing a `reduce_sum`. In general, SIMD reductions inside a tight loop are a perf anti-pattern, and you should try and figure out a way to make those reductions `element-wise` and not `vector-wise`. This is what we see in the following snippet:
62+
63+
```rust
64+
#![feature(portable_simd)]
65+
#![feature(array_chunks)]
66+
use std::simd::*;
67+
68+
//#[target_feature(enable = "avx2")]
69+
pub unsafe fn dot_prod_simd_1(a: &[f32], b: &[f32]) -> f32 {
70+
// TODO handle remainder when a.len() % 4 != 0
71+
a.array_chunks::<4>()
72+
.map(|&a| f32x4::from_array(a))
73+
.zip(b.array_chunks::<4>().map(|&b| f32x4::from_array(b)))
74+
.fold(f32x4::splat(0.0), |acc, zipped| acc + zipped.0 * zipped.1)
75+
.reduce_sum()
76+
}
77+
```
78+
79+
In `dot_prod_simd_1`, we tried out the `fold` patter from our previous `scalar` code snippet examples. This pattern, when implemented via SIMD instructions naively, means that for every `f32x4` `element`-wise multiplication, we accumulate into a (initially `0` valued `f32x4` SIMD vector) and then finally do a `reduce_sum` at the end to get the final result. This
80+
81+
82+
-----
83+
84+
Now we will exploit the `mul_add` instruction. Open [this link to view the snippets side by side once again](https://rust.godbolt.org/z/vPTqG13vK). We've started off with a simple computation: adding and multiplying. Even though the arithmetic operations are not complicated, the performance payoff can come form knowing specific hardware capabilities like `mul_add`: in a single instruction, it can multiply 2 SIMD vectors and add them into a 3rd, which can cut swaths in the data movement overheads `xmm` registers can carry. Other instructions like inverse square roots are available (which are very popular for physics calculations), and it can get oodles more complex depending on the problem - there's published algorithms with `shuffles`, `swizzles` and `casts` for [decoding UTF8](https://arxiv.org/pdf/2010.03090.pdf), all in SIMD registers and with fancy table lookups. We won't talk about those here, but we just want to point out that firstly, reading the books can pay off drastically, and second, we're starting small to show the concepts, like using `mul_add` in the next snippet:
85+
86+
87+
```rust
88+
#![feature(portable_simd)]
89+
#![feature(array_chunks)]
90+
use std::simd::*;
91+
92+
// A lot of knowledgeable use of SIMD comes from knowing specific instructions that are
93+
// available - let's try to use the `mul_add` instruction, which is the fused-multiply-add we were looking for.
94+
#[target_feature(enable="sse")]
95+
pub unsafe fn dot_prod_simd_2(a: &[f32], b: &[f32]) -> f32 {
96+
assert_eq!(a.len(), b.len());
97+
// TODO handle remainder when a.len() % 4 != 0
98+
let mut res = f32x4::splat(0.0);
99+
a.array_chunks::<4>()
100+
.map(|&a| f32x4::from_array(a))
101+
.zip(b.array_chunks::<4>().map(|&b| f32x4::from_array(b)))
102+
.for_each(|(a, b)| {
103+
res = a.mul_add(b, res);
104+
});
105+
res.reduce_sum()
106+
}
107+
```
108+
109+

0 commit comments

Comments
 (0)