3 ms·
> the first line is now trivially parallelizable by most auto-vectorizers It’s not that trivial to vectorize, would be something like this for SSE: const
by Const-me 6y ago
> the first line is now trivially parallelizable by most auto-vectorizers
It’s not that trivial to vectorize, would be something like this for SSE:
const __m128 x = _mm_loadu_ps( px );
// Rotate the lanes
__m128 xPrev = _mm_shuffle_ps( x, x, _MM_SHUFFLE( 2, 1, 0, 3 ) );
// Insert the highest lane of previousStepX into lowest lane of xPrev
// The IMM number means extract lane 3, insert into lane 0, don't zero out any lanes
xPrev = _mm_insert_ps( xPrev, previousStepX, 0b11000000 );
const __m128 halfSum = _mm_mul_ps( _mm_add_ps( x, xPrev ), _mm_set1_ps( 0.5f ) );
previousStepX = x;
Which compiler do you think would do that automatically?
- dragontamer 6y agoWhy would you need to rotate the lanes? const __m128 x = _mm_loadu_ps( px ); const __m128 xPrev = _mm_loadu_ps( px + 4 ); _mm_loadu_ps: load-unaligned. There's no requirement for addresses to load/unload. There are probably penalties for loading across a cache line, but its perfectly valid to do this. ----------- > Which compiler do you think would do that automatically? GCC simply does the unaligned load: https://godbolt.org/z/2HwYMi https://godbolt.org/z/2HwYMi Clang does the shuffle successfully: https://godbolt.org/z/Y3hbQn https://godbolt.org/z/Y3hbQn The shuffle is probably optimal... I'm surprised CLang is good enough to recognize the opportunity. But simply doing 2x loads from memory is probably fine, especially because the 2nd load is guaranteed to be from L1 cache. Besides, assuming 128-bit vectors, 2x 128-bit vectorized loads every 4-elements (averaging 1/2 a load per element) is still superior to scalar code (1-load per element). Honestly, Clang surprised me by generating the superior shuffle code. I was expecting both GCC and Clang to generate the suboptimal (but simpler) unaligned loads.
- Const-me 6y ago> Why would you need to rotate the lanes? To half the count of RAM access instructions. Even L1D cache hit is still slower than these two instructions for shuffle and insert, both are quite fast, they deliver the result on the next cycle. > GCC simply does the unaligned load Right, that’s twice as many loads as necessary. > Clang does the shuffle successfully I wouldn’t call that code “successfully”. Clang uses shufps instructions to shuffle integer vectors. On many CPUs, especially older than a couple of years, there’s a non-trivial latency, a few cycles, to pass data between integer and floating-point execution units. The approach is correct with these shuffles, but not the implementation. For integers it should have used at least _mm_shuffle_epi32 = pshufd or _mm_slli_si128 = pslldq, but ideally _mm_alignr_epi8 = palignr from SSSE3.
- dragontamer 6y agoI guess we have different measures of "success". I don't expect any compiler to beat me in intrinsics or hand-crafted assembly. After all, I can use the compiler. But the compiler can't use my brain. So in all circumstances, I will always at least tie the compiler. Its an "auto-vectorization success", because the pseudo-code I've laid out auto-vectorizes to SSE 128-bit, AVX 256-bit, and AVX512 512-bit code. Furthermore, CLang supports many architectures, and probably can convert my simple for-loop into SIMD on other architectures without any intervention (ie: ARM Neon) The code doesn't need to be optimal, it just needs to be faster than the scalar form and portable.
- Const-me 6y agoYes and no. “yes” because what clang did is a very good job, pretty sure it substantially faster than scalar code. Especially on newish CPUs like Zen2. “no“ because from what they have currently for that particular test case, they only need a few improvements to make their code as fast as a manually vectorized version. Apparently, the compiler is already aware of the cost of RAM loads. They just need to account for these bypass delays between integer and FP execution units in their code generator.
- nkurz 6y agoFrequently, when one is using SIMD one is more concerned about throughput (how many operations you can do per second) and less about latency (how many nanoseconds until you get there first result back). I'm not sure if you are equating the two in your comments, or have an actual preference for lower latency, but realize that one can often gain in throughput by giving up some latency. I mention this because I don't share your certainty that avoiding duplicate loads is necessarily "faster". I've occasionally been able beat compilers at getting higher throughput by making the duplicate load, despite the fact the latency increases. This is because vector shuffles (and I think aligns and inserts) are limited to a single execution port (at least on recent Intel processors), while loads can be executed twice per cycle. Depending on the mix of the rest of instructions, this means that sometimes the redundant load ends up being the better choice. Which is to say, rather than being sure, measure!