Optimizing loops in C for higher numerical throughput and for fun
lshift.net
lshift.net
Relative to the amount of code in the world there are vanishingly few loops which have this kind of 10x speedup just waiting for the right tuning.
On this specific case it was developer helping the gcc optimizer. These type of optimizations could produce worse code in another C compiler.
Sometimes the code is already fast enough for the problem being solved, other times rewriting the algorithm could improve performance.
Only in cases where every ms counts, does helping the compiler really matter.
But the 1733.4 MFLOPS solution is a really naive one. Data locality and cache friendliness are two of the first things taught in high performance classes, with an obvious trick : iterating using row-major order like in the second case.
This trivial optimization gives a 9419.8 MFLOPS throughput. I don't think "everyday code" like that is so inefficient given the maximum result which can be obtained (17985.4 MFLOPS).
As an example, while doing code for a 6809-based video game (see [1] for full war story), I had to use every single register on the processor, including the system-stack register and the direct-page register, to speed up a graphics routine. This meant that, while my code was running, the system effectively had no stack and couldn't use certain addressing modes. Further, it also meant that interrupts would corrupt whatever memory the stack register happened to be pointing to.
All these problems were solvable – and the pain we had to go through to solve them was, for us, absolutely worth the 70% speed-up it bought us – but no compiler writer is going to incorporate tactics this weird into a compiler's optimization logic. If you need to go "full weird," assembly is often the only way.
[1] http://blog.moertel.com/posts/2013-12-14-great-old-timey-gam...
It's a shame that C prevents these optimisations by default. I suppose that a quick runtime check could see if the arrays overlapped and could then switch to the faster loop style, but the compiler would have to decide whether the loop was important enought to be worth generating the two code paths for.
However, for simple iteration, if you use iterators idiomatically instead of C-style for loops then you won't incur any bounds checks.
f(&mut x as *mut u8, &mut x)
and there is no dynamic borrowing checking for that.Would it become some form of undefined behaviour to have an *mut aliasing with an &mut?
Of course, if you transmute stuff into `&mut T`, then you'd better make sure that those two `&mut T`s don't overlap. But `﹡mut T` should be fine.
It pretty much solves the whole issue.
(It's also not technically in C++, although in practice it can be achieved through extensions.)
float dot(std::valarray<float> const& x)
{
return (x*x).sum();
// Alternative: inner_product(begin(x), end(x), begin(x), 0.0f);
}
should, in theory, achieve the same kind of performance as the C version (but none of this is guaranteed, sadly). A smart implementation would actually end up calling BLAS routines, which is what libraries like Eigen do.Which reminds me: does Rust have infrastructure that would allow something like expression templates to exist?
> Meanwhile, the Fortran programmer writes y = dot_product (x, x) and moves on to the interesting bits. Plus, if auto-parallelization is on, or if this is in an OpenMP workshare section…
This is titled "optimizing loops in C" but this level of optimization is actually programming in assembly language, specifically x86 with SSE extensions.
However, someone had to implement dot_product and cblas_sdot at some point (either in the compiler or in the library), and they need to be rewritten from time to time for new architectures. More to the point, most programmers, most of the time, aren't just doing a dot product. They're doing some other more sophisticated computation for which these techniques may be quite relevant. The dot product is just a convenient example.
However, the author starts by talking about a "holy war between C and Fortran" and then proceeds to write... well, assembly language using the C compiler. So the summary could be "assembly language can be made more efficient than Fortran, and C lets you coax the compiler into writing the assembly language you want". I guess this could be seen as a win for C, but I'm not so sure...
for (i = 0; i < M; i++) {
for (j = 0; j < N; j++) {
x[i][j] = ...;
}
}In GCC, the only option which enables -funroll-loops (apart from explicitly enabling it) is -fprofile-generate which is GCC's profile guided optimization.
The reason it enables -funroll-loops is that since it gathers runtime statistics during the profiling run it has enough information to accurately perform loop unrolling without risking performance degradation.
Anyway, goes to show you still need to look at the disassembly and do plenty of profiling with Fortran binaries too. No such thing as a free lunch.
It should be also noted that this can be trivially implemented in direct SIMD using GCC/clang vector extensions, without having the compiler 'guess' the SIMD part, which btw would be also make the code much easier to read.
The original author's code isn't available for this example, but I put together something I think is comparable. I may still have silly bugs, but here are my initial result on Sandy Bridge are something like:
icc 13.0.1 -03 -march=native -fno-inline wrong-loop: 1.35 s
icc 13.0.1 -03 -march=native -fno-inline right-loop: 0.78 s
icc 13.0.1 -03 -march=native -finline-functions wrong-loop: 0.22 s
icc 13.0.1 -03 -march=native -finline-functions right-loop: 0.22 s
gcc 4.8.0 -03 -march=native -fno-inline wrong-loop -fno-inline: 1.38 s
gcc 4.8.0 -03 -march=native -fno-inline right-loop -fno-inline: 1.14 s
gcc 4.8.0 -03 -march=native -finline-functions wrong-loop: 1.35 s
gcc 4.8.0 -03 -march=native -finline-functions right-loop: 1.14 s
There are all sorts of things I might be doing differently (or wrong), but I'm printing out a total-of-totals so I know it's at least going through the loops. It's possible that is a fast-math optimization, but I wouldn't be betting on GCC -O3 to be close to optimal.The problem with inline assembler is that it is almost untouchable by the optimizer. By adding some inline asm, you may inhibit a lot of optimization that could give better perf overall.
For this kind of tasks it is often a lot better to use intrinsics (e.g. xmmintrin.h for SSE) or use compiler extensions __attribute__((vector_size(16))) etc. This way you can utilize the CPU features you have available while still allowing the optimizer to do high level optimizations.
But I'm certainly no expert in this area, so take my opinion with a large grain of salt.
I made a simple test void nsum(float v, float acc, int n, int vc ) { int j, i; for(i = 0; i < n; i++) for(j = 0; j < vc; j++) acc[i] += v[j][i]v[j][i]; }
And then I tested the same function with a different declaration void nsum(float * restrict * v, float * restrict acc, int n, int vc )
The version without restrict qualifier had 1.01s runtime. Version with restrict had 0.45s runtime. Both were compiled with identical flags (just -O3) using the ancient gcc 4.4.5. (vectorizer is enabled by default at O3 even in this version).
That's 2x speedup with a simple pointer definition.
Without it the code of sum_of_squares_1 is as following:
400913: f3 0f 11 07 movss %xmm0,(%rdi)
400917: f3 0f 10 48 34 movss 0x34(%rax),%xmm1
40091c: f3 0f 59 c9 mulss %xmm1,%xmm1
400920: f3 0f 58 c8 addss %xmm0,%xmm1
400924: f3 0f 11 0f movss %xmm1,(%rdi)
400928: f3 0f 10 40 38 movss 0x38(%rax),%xmm0
40092d: f3 0f 59 c0 mulss %xmm0,%xmm0
400931: f3 0f 58 c1 addss %xmm1,%xmm0
400935: f3 0f 11 07 movss %xmm0,(%rdi)
400939: f3 0f 10 48 3c movss 0x3c(%rax),%xmm1
As you can see it stores the dst[y] on each iteration. With function definition of:
void sum_of_squares_1(float dst[restrict ROWS], float src[restrict ROWS][COLS])
The disassembly becomes completely different. However the speed of the end result did not really change that much.Could you throw objdump -d of the best icc output to pastebin? I'm interested to see what kind of code it produces.
Late night here in California. Good night!