3 ms·
Regular compilers are actually extraordinarily good at auto-vectorizing. There are a few oddities that one has to be aware of; but in my experience, if you offe
by rerdavies 1y ago
Regular compilers are actually extraordinarily good at auto-vectorizing. There are a few oddities that one has to be aware of; but in my experience, if you offer a GCC or Clang compiler an opportunity to auto-vectorize, it will leap on it pretty ruthlessly. I have done a lot of work recently with auto-vectorizing code for high-performance realtime audio processing. And it's pretty extraordinary how good GCC and Clang are. (MSVC is allegedly equally good, but I don't have recent experience with it).
And they will generally produce better assembler than I can for all but the weird bit-twiddly Intel special-purpose SIMD instructions. I'm actually professionally good at it. But the GCC instruction scheduler seems to be consistently better than I am at instruction scheduling. GCC and Clang actually have detailed models of the execution pipelines of all x64 and aarch64 processors that are used to perform instruction scheduling . Truly an amazing piece of work. So their instruction scheduling is -- as far as I can tell without very expensive Intel and ARM profiling tools -- immaculate across a wide variety of processors and architectures.
And if it vectorizes on x64, it will probably vectorize equally well on aarch64 ARM Neon as well. And if you want optimal instruction scheduling for an Arm Cortex A72 processor (a pi 4), well, there's a switch for that. And for an A76 (a Pi 5). And for an Intel N100.
The basics:
- You need --fastmath -O3 (or the MSVC eqivalent).
- You need to use and understand the "restrict" keyword (or closest compiler equivalent). If the compiler has to guard against aliasing of input and output arrays, you'll get performance-impaired code.
- You can reasonably expect any math in a for loop to be vectorized provided there aren't dependencies between iterations of the loop.
- Operations on arrays and matrices whose size is fixed at compile time are significantly better than operations that operate on matrices and arrays whose size is determined at runtime. (Dealing with the raggedy non-x4 tail end of arrays is easier if the shape is known at compile time).
- trust the compiler. It will do some pretty extraordinary things. (e.g. generating 7 bit-twiddly SIMD instructions to vectorize atanf(float4). But verify. Make sure it is doing what you expect.
- the cycle is: compile the C/C++ code; disassemble to make sure it did vectorize properly (and that it doesn't have checks for aliased pointers); profile; repeat until done, or forever, whichever comes first.
- Even for fairly significant compute, execution is likely to be dominated by memory reads and writes (hopefully cached memory reads and writes). My Neural net code spends about 80% of its time waiting for reads and writes to various cache levels. (And 15% of its time doing properly vectorized atanf operations, some significant portion of which involves memory reads and writes that spill L1 cache). The FFT code spends pretty close to 100% of its time waiting for memory reads and writes (even with pivots that improve cache behavior). The actual optimization effort was determing (at compile time) when to do pivot rounds to improve use of L1 and L2 cache. I would expect this to be generally true. You can do a lot of compute in the time it takes for an L1 cache miss to execute.
I can't think of why ISPC would do better than GCC,given what GCC actually does. My suspicion is that ISPC was a technology demonstration rather than a real product produced at a time when major compilers had no support at all for SIMD.
- torginus 1y agoI think the issue with your argument is their optimizations are brittle, due to the complex limitations needed to vectorize code, as well as subtle compiler limitations. Their optimizations also do not necessarily carry across compilers. If you are in gamedev, and are targeting multiple platforms, with multiple CPU architectures and compiler vendors, and your game relies on a particular function being 20x faster than the scalar version, then failing to vectorize is a blocker bug. Imo due to this, autovectorization is more of a nice surprise than something you can rely on.
- rerdavies 1y agoIt boils down to: does it take longer to verify that the compiler did the right thing on each platform/architecture/processor you're interested in than it does to write hand-coded assembler which will be tightly tuned to a specific processor, and will not have proper instruction scheduling anyway, because humans just cannot do that anymore (and would have to be done all over again anyway for next year's Gen 16 intel processors that now have 7-wide instruction decoding stages instead of 5-wide instruction coding stages. As for fragility, if it's an obvious candidate for a SIMD loop, all the compilers I have worked with so far will obviously auto-vectorize it. What pushed me to the point of no going back: checking to see how they were doing it and finding complete models for the complete execution pipelines of literally hundreds of processor, and realizing that the reason why their code does such good instruction scheduling was because they had a full model of the execution pipeline! In both GCC and Clang sources. How long has this stuff been around for? A decade and a half? Two? The Compiler Kiddies needed SOMETHING to keep them occupied and employed for 20 years. And auto-vectorization was it. A major industry-wide initiative, specifically to address compiler auto-vectorization. AMAZING stuff. (Well. That and continuous never-ending C++ standards. But a LOT of auto-vectorization. And instruction scheduling).
- the8472 1y agoGood at autovectorizing? Eh, if you massage your code in godbolt until it kicks in. But just naively writing code often falls off the happy path. And even when it autovectorizes it can sometimes produce such gems as vmovdqu xmm1, xmmword ptr [rdx] vmovdqu xmm0, xmmword ptr [rdx + 16] mov rax, rdi vmovd ecx, xmm1 or byte ptr [rsi], cl vpextrb ecx, xmm1, 1 or byte ptr [rsi + 1], cl vpextrb ecx, xmm1, 2 or byte ptr [rsi + 2], cl vpextrb ecx, xmm1, 3 or byte ptr [rsi + 3], cl vpextrb ecx, xmm1, 4 or byte ptr [rsi + 4], cl vpextrb ecx, xmm1, 5 or byte ptr [rsi + 5], cl ... vpextrb ecx, xmm0, 15 or byte ptr [rsi + 31], cl vmovups ymm0, ymmword ptr [rsi] vmovups ymmword ptr [rdi], ymm0 instead of a vorps