Haskell Beats C Using Generalized Stream Fusion [pdf]
research.microsoft.com
research.microsoft.com
In comparison, the performance they're getting out of Haskell is coming from comparatively mundane, easy to understand code. That's pretty impressive.
Or another way of putting it: It looks like naive Haskell outperforms naive C++, and naive Haskell is not that much slower than carefully-tuned C++.
template < typename Derived >
typename Derived::Scalar norm2( const MatrixBase <Derived >& v ) {
return v.dot(v);
}
Why is it a template function? Why does it return Derived::Scalar instead of double? Dear god, Eigen is magic.Without the templated function, the compiler would first generate the subtraction into a temporary vector, and then compute the dot product on that vector.
edit: I see, they address this in the article: In this specific case the Eigen library has the squared L2-norm in its API, but in more complicated examples there might be no such built-in provision
Uh oh.
And now the question, do you really want to an unproven and unstable Haskell framework? While there are stable alternatives (lake Eigen, Theano, etc) available?
So no. Haskell (production ready Haskell) doesn't even come close.
That would imply that the C version of the C++ code would be just as fast, if only it were not "poorly written".
However there are some types of code (including this kind) where having C++ available to generate the code for you makes it almost inherently faster than C.
You could certainly reimplement specific matrix operations for very specific types in C, but the C++ Eigen library does this all for you at compile time, and does it the best way for each combination of types and operations.
If you do this in C, even with macros everywhere, it's going to be much more difficult, especially since you can't (AFAIK) declare and discard temporary types within macros, though C11 at least allows you to refer generically to types within macros. The canonical example for this kind of disparity is C's qsort() vs. C++ templated sorts.
This is kind of the point to the paper. Performance of this sort is only possible in C++ if you're careful to follow the instructions provided with Eigen about using templated functions, and some very smart devs had to write Eigen in the first place. The Haskell code comes fairly close starting from the source code you'd write naïvely, and without tons of optimization on the stream fusion code generator.
The paper would have been better off with more complex examples (e.g. functions that can't be trivially implemented in asm by hand with no register pressure) and fewer comparisons to implementations that could be doing what they are, but aren't.
The more complex examples are in Section 5.2; see Figure 8. Granted, we would have liked to have done more, but deadlines are deadlines...
There are some other CPU-related slight inaccuracies in the paper. Prefetching is repeatedly mentioned, even though its effect is negligible when one has a perfectly linear memory access pattern; unaligned loads are mentioned as a performance hit, but they are essentially free on the test processor (2600k, Sandy Bridge).
Matrix multiplication would perhaps be a better example to show the power of clever prefetching.
all that said, it is still very impressive work!
Perhaps somebody with an AVX capable processor can redo the two benchmarks with correct compiler options (i.e. -mavx) and/or with ICC, and replacing the C code of the second benchmark with code that is equivalent to the Haskell code:
double s=0;
for(int i=0; i<n; i++) s += pow(a[i]-b[i],2);I found out gcc supports -fprefetch-loop-arrays, although it is not guaranteed to have a positive effect. In this case, it does appear to run faster than without prefetching. AVX versions also run faster than the standard SSE counterparts even with 128 bit registers. ymm regs are faster than xmm.
gcc is faster than icc actually, and icc runs faster with the plain c version than the vectorized versions. I can't figure out how to make icc prefetch either, the switch that controls it doesn't seem to have an effect.
Is the probability really so high as to be 100% that all optimization tests were against a poorly written competitor? That's amazing if that's the case.
So, while some researchers have gotten it such that a relatively straight forward implementation in a new language with new tools is better than the old way, the folks working the old way have not exactly stagnated.
This seems especially likely when you consider that for many of the really fast implementations, it is not that they are using no abstractions. The abstractions they are using are very close to the machine, because the machine is the abstraction they are using. (That make sense?)
double s = 0;
for(int i=0; i<n; i++) s += pow(a[i]-b[i],2);
return s;
But they did not use this C code. The C code they used made 3 calls to the BLAS library. The end result is that the C code is doing something equivalent to this: double x=0, y=0, z=0;
for(int i=0; i<n; i++) x += a[i]*a[i];
for(int i=0; i<n; i++) y += a[i]*b[i];
for(int i=0; i<n; i++) z += b[i]*b[i];
return x - 2*y + z;
While this does return the same result (assuming infinite precision arithmetic) it is obviously not the way anybody would do it, since it's doing 3 times the work. Even worse, because their code is calling into the BLAS library for each loop, the compiler is explicitly prevented from optimizing the three loops and memory accesses by combining them into one. Note that the Haskell code is doing the efficient single loop. So yes, it is poorly written code.The 1x BLAS that uses 2x RAM gets shafted, presumably by cache lossage.
That said, a lot of benchmarks are just completely bogus. It's surprisingly common to see someone claim that they're beating the "state of the art" when they're comparing against a weak baseline. One problem is that people don't understand the systems they're benchmarking sufficiently well to obtain good results.
In this case, I have no doubt the results are much better than they've obtained in Haskell before - I think they've done an impressive job and I don't doubt the value of the work. But if the question is whether rewriting your C code in Haskell would make it go faster... well... I wouldn't bet the house on it.
This quote seems a touch odd when you consider just how much faster the C++ solution is. The Haskell programmer definitely does not get a complete pass on worrying about the implications of abstraction. Indeed, this seems to underscore that if performance is a major criteria, than language choice still matters.
For example, in Haskell you can freely rewrite common subexpression with a let whenever you want and in whatever order you want.
--original
(f x) ... (f x)
--let
let y = f x in y ... y
On the other hand, if your language has side effects these sort of transformations are not always safe.Fair enough, but lazyness also plays nicer with things like `if` and `&&` that comput things lazily. (Without lazyness you need to add lots of wrapper anonymous functions).
Another thins is that, from a hystorical point of view, the only reason Haskell managed to be so pure in the first place was the lazyness. If your language is not lazy its very tempting to add sideeffects.
It seems similar to what plinkplonk is talking about.
Mu is a great example. I wish there was more information about it publically available.
I have never met a single person who has said this, let alone capable of following through.
If you are capable, I am envious of your determination/natural talent. Or alternatively I and every programmer I know are idiots. Actually both seem like possibilities.
Most programmers haven't taken the time to actually learn Haskell.
Haskell only seems hard at first because it incorporates a lot of ideas that other languages do not. For a lot of developers, learning Haskell is a little bit like learning to program from scratch all over again.
In that sense, yes, I guess it's hard, but so was whatever language you learned first.
I find this to be the greatest joy of learning a new paradigm, everything is new and unknown, so even simple things are fun (again).
For example once you have a working program and decide that actually some value could be read/written using a file instead of computed at runtime and the place you use it is 10 levels deep after calling a non-IO function, you've got two choices. Unsafe IO (then why not use it everywhere) or a minor program redesign. Haskell is the only language where I run into this kind of situation. (it's still quite simple despite this)
Have you tried the School of Haskell, it incorporates the minimum viable skills for learning haskell (pulling code apart in the REPL, and understanding type signatures, pattern matches and compiler type check errors)
https://www.fpcomplete.com/school/how-to-use-the-school-of-h...
Also, looking at the results most of the performance comes from which library you are using, and how well it is optimised. No compiler is good enough to do what the good libraries do.
That's nonsense. And if you take a look, say, at NumPy, which uses heavily tuned BLAS/LAPACK code, and Numexpr, which uses the "compile the snippet and run it" approach, you'll see why. Libraries won't save your performance if you can't optimize across procedure and module boundaries.
from what i understand of section 3.2 the consumer is critical in selecting the correct stream type from the bundle. who decides that? does the user have to select the appropriate function? or does it some how fall out naturally from the use case? or does the compiler select it?
Of course the programmer can also use the lower-level stream interface directly if desired, but then the programmer must also know which stream representation to choose.
icc's not actually that much better than gcc in optimization, it's primarily a much better runtime library that gives icc its speed boost.