Beating the compiler
roguelazer.com
roguelazer.com
I have a different opinion on those 3ms optimizations:
"It all adds up"
Stopping at one 3ms optimization is not going to move mountains. But doing that 10 times is already 30ms. Imagine that you found 100 micro optimizations. Now we are talking!
So keep calm, optimize on. This is how compilers get better over time.
One of my favorite examples is Ubuntu - they run their 'One Hundred Papercuts' program [1], which is about fixing and optimizing little things that on their own would never get fixed, but together they are more than the sum of their parts in terms of user experience.
http://permalink.gmane.org/gmane.comp.db.sqlite.general/9054...
Occasionally performance is a feature, but a lot of times it's just an excuse for developers to have fun writing assembly.
300ms still doesn't put a dent in 8 or 9 seconds.
> So keep calm, optimize on. This is how compilers get better over time.
So work with the compiler rather than against it. As others have pointed out, with the correct -march setting you get code that's just as good as this hand-tuned assembly - and much more maintainable, and the correct compiler setting will improve the rest of your code as well.
>>> l=[0xffffffff, 17]
>>> sum(l)
4294967312
C based code will silently overflow/truncate, giving 16 in the example above. The author handwaved all this away, but it does show the usual tradeoffs between right/robust answers and fast answers.Undefined behavior allows for aggressive optimizations where the compiler assumes the integer overflow can never happen, leading to such wonderful bugs as this:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475
Some compilers may offer specific settings, such as fwrapv, which give you a way to define the behavior of integer overflow. (Although last I read fwrapv was broken.) I see no mention of any such specific setting in the article. If you've spotted one I've missed, please call it out more specifically.
He hand waved it away because it's not really relevant to the point.
Robust, correct and fast are a hard combination to do. I do acknowledge the article is about the latter.
This assumption is common among authors of C code, but is sometimes also exploitably incorrect. Even if you really don't care about supporting things larger than a certain size, you do still have to correctly account for overflow, not just ignore it.
loop:
acc += A
acc += B ...
was inefficient because each add instruction is dependent on the one before it. The CPU has to pause constantly to wait for the result in acc in order to continue.The correct way to do this is to have 8 accumulators (or whatever the loop unrolling depth is) and then to sum those together at the end. This helps to keep the processor's pipelines full.
loop:
acc1 += A
acc2 += B
acc = acc1 + acc2
The author's use of SIMD instructions is even better still, where multiple variables were used. However intrinsics would have been far more readable.For further speed improvements, streaming intrinsics (since all reads are only done once, and never written to) could be useful. Also OpenMP for multithreading would be a good fit here.
It's also possible that you find out that if you enable --generate-for-haswell or some other arcane compiler flag, it'll do it for you.
When you're relying on the compiler to vectorize, you run the risk of a subtle, innocuous change to the code breaking the vectorization -- and this will happen a lot. Also, when you target multiple compilers, it's very difficult to get reliable performance across all of them, unless you do the vectorizing yourself.
Not to mention, compilers tend to do great on simple test cases like these, but totally barf as soon as the loop becomes more complex (Try adding some conditionals to the loops some time... It's not that these loops can't be vectorized, it's just that the compiler doesn't know how).
To get the best performance out of vectorization, it's mostly about organizing the data so that it can be easily vectorized. If you've gone through this work, it's fairly pointless not to take the extra effort to guarantee that you're getting the performance you expect.
I think the best solution is to be able to make some kind of annotation, or other way of declaration, on a function that says "this function should be no worse than this". In Scala's case, for example tailrec. I'm unfortunately having a hard time with coming up with other, specific examples, but the gist of it is that the compiler either manages to do all the work on the function itself and the functions that that function calls, or errors out and reports what it couldn't do. Ideally I would want to make 10 functions which are all pure and referentially transparent, call all those functions from a top function with some kind of annotation that gives some demands with regards to optimizations, and then have that function be transformed to a single, efficient, fused loop with no allocations or intermediary values that are unnecessary. But like I mentioned, the hard part seems to be in actually specifying what your demands are.
The big issue (aside from convincing MSVC to implement it ;) with your suggestion is that, unlike TCO, vectorization isn't really a boolean. There's a range of what vectorization might mean (you can vectorize code and do a bad job with it, only marginally beating out the scalar code), so you'd still need to check the generated code for the situations where you care.
And honestly, it's not worth the effort. Vectorization shouldn't be as scary as it is for most programmers. Once you get the hang of how to do it, it's not bad at all. We write a lot of SIMD code at work, and 'difficulty of writing SIMD code' isn't a big issue for us. Honestly, it's kind of fun, a bit like solving a puzzle (an optimization puzzle, something like SpaceChem or Infinifactory).
Now, a situation where it might be a win is if you have a lot of different platforms you need vectorized code for... but in my experience you're probably better off doing it by hand unless this is a huge number.
Most platforms offer intrinsics.
> it's a maintenance risk unless everyone in the shop knows how to write and maintain SSE code.
This is understandable, but not the case where I work. If you need to be writing SIMD code and this is the case, then you need to hire programmers who can do it. That or convince them to learn, as (again) it's not that hard.
But IMO it's a nice article anyways. I'd wager the final implementation he comes up with is faster than PyPy.
People in the comments who tried a reasonable numpy version found it to be around the same speed as the unoptimized C version.
What if my list is: [2000000000, 2000000000, -2000000000, -2000000000]
But it won't work for {2000000000, 2000000000}, as stated in the article.
Should be Seymour Cray's 6600, as it was sold by CDC [1]
In particular, see vDSP_sve and vDSP_sveD which compute single-precision and double-precision vector sums respectively.