On the Cleverness of Compilers
alexey.radul.name
alexey.radul.name
There were several occasions where I'd write out some elegant math and think "Yeah, that's pretty. But, it would run 30% faster if I transposed everything. But, then I'd have to hand-interleave the internals of all these functions..." Then I'd check the assembly output and Bam! the compiler was already producing exactly what I had in mind!
The downside was that it took several minutes to compile <100 line sources that produce <1000 instruction programs. That's with a language completely specialized around small-scale linear algebra that maps very directly to hardware that is completely specialized around small-scale linear algebra; with branching highly discouraged, all functions calls expected to inline away completely, no recursion, no pointers, no heap, no imports, tiny-if-any headers and a tiny standard lib.
It's my understanding that most of the smartness of the 360's HLSL compiler came from using O(n^3) analysis algorithms that were simply impractical at a larger scale. Our game had thousands of shader programs that were mechanically specialized for different situations. As a result, recompiling all of them after changing the tiny shared header took many hours. "Thankfully" the C++ of the game took so long to compile that we were already using Xoreax Incredibuild to distribute the compilation. We adapted that setup to distribute shader compilation and brought the turn-around time down to 15 minutes by leaching cycles from several dozen of my co-workers' machines.
To make that concrete, control-flow analysis (which answers "which possible functions could I call through this variable?") is cubic in general. But if you cut it off to only tracking N separate functions and then giving up to ANY when you hit that N, you can cut off the few bad cases (e.g., all of the bindings to the function passed to map, which you will probably just inline later anyway) that generate the cubic behavior and keep your execution time well-behaved, even for whole program compilers, at very small loss of precision in practice.
http://dl.acm.org/citation.cfm?id=315891.315934
A good survey of many of the tricks used in practice (and the formal techniques, as well) is available in Jan Midtgaard's tome, which was finally published in the last year or so:
This sounds like the Stalin Scheme compiler.
Simple benchmark (vs C (gcc), Stalin wins): http://justindomke.wordpress.com/2009/02/23/the-stalin-compi...
GUI addon: http://code.google.com/p/stalingui/
http://community.schemewiki.org/?Stalin
Download link: https://engineering.purdue.edu/~qobi/software.html
> Simple benchmark (vs C (gcc), Stalin wins)
Did you read the comments?
> Nice, but part of the reason the C version is slow is that you are using abs instead of fabs. abs takes an int as an argument (this should be a compiler warning!) – passing a double other than zero always returns a large number. The C version is basically iterating till the error is below double’s resolution.
> Excellent point! Making the switch to fabs brings the gcc-inline speed down almost exactly to what Stalin is getting.
But we can make that not matter if we rethink the relationship between the compiler and the programmer. What if you got the first version of your binary very quickly, but then you could let the compiler run arbitrarily long, and watch it continue to produce better and better versions?
What if the compiler could compile not just source code, but diffs of source code? It could apply your one-line change without redoing its exhaustive analysis of all the things that you couldn't have effected.
Compilers try hard to avoid whole-program analysis, but I think that's misguided. Spend the cycles, they're getting cheaper all the time. If you can update your analysis incrementally as the programmer works, you're golden.
Compiler flags are all you need to solve that, no?
That said, check out Julia: for any function, you can see how it looks at each step of compilation, down to assembly: source, optimized source, type inferred source (for given input types; it is a dynamic language), LLVM bytecode, and assembly. All visible and introspectable at runtime from either the REPL or scripts.
I love that sentence. A sharp-witted summary of the recent imperative vs. functional discussion.
But OTOH I recently worked on porting some ancient non-modular imperative code to work on newer systems and I have to agree that imperative programs can be evil.
Expression related optimizations: Constant Folding, Constant Propagation, Global Propagation, Strength Reduction, Common Subexpression Elimination, Partial Redundancy Elimination, Induction Variable Elimination, Reassociation
Loop related optimizations: Loop Invariant Code Motion, Loop Peeling, Loop Unrolling, Loop Distribution, Loop Autoparallelization, Loop Fusion, Loop Fission, Loop Interchange, Loop Tiling/Stripmining, Vectorization, Scalarization
Memory/cache related optimizations: Cache blocking, False Sharing Elimination, Structure Peeling, Structure Splitting, Array Contraction, Multi-dimensional Array Dimension Reordering
Control flow related optimizations: Code block re-ordering (by frequency), Branch prediction (by static analysis/feedback guided), Code hoisting/sinking (to optimize CPU pipeline), Automatic Inlining, Tail Call Optimization
Code generation related optimizations: Register allocation (np complete), Peephole optimization, Superoptimization (no one really does this yet, but cool nonetheless)
Analysis (interprocedural/intraprocedural): Alias Analysis (flow/context (in)sensitive), Points-to Analysis, Escape Analysis, du chain Analysis, Live Variables Analysis, Memory Access Pattern Analysis (to guide memory layout optimizations), Available Expressions/Copies Analysis, Loop Dependency Analysis, Control Flow Analysis, Dominator data flow Analysis, Globals Analysis
Miscellaneous: Static Single Assignment, Data Flow Analysis (in, out, transfer, meet!), Worklist Algorithm (for Data Flow Analysis!), Symbolic Execution/Abstract Interpretation
Writing straightforward code is the best thing you can do for a compiler. They can tell exactly what you want to do, and do their best to generate the fastest/most memory efficient code possible. Help compiler help you!
Also, compiler usually comes with several different levels of optimizations. And you can tune each one of these optimizations, like you would with a racing car. Read the manual!
It's so mysterious... It's developed by this one guy. The only thing you can find on his website is a tarball [2]. It's like programming mythology.
[1] http://community.schemewiki.org/?Stalin [2] https://engineering.purdue.edu/~qobi/
And there we have a clue as to why, in modern computing, modular code is not high-performance code. The very advantage of modularity is the source of its performance cost: modularity is the ability to do things other than what you did. Modularity is the ability to reuse the same idea in many different ways and in many different places.
PROSE seems to have been completely forgotten; I can find nothing on it now. But I distinctly recall that it did AD.
Everything in computer science was invented before 1980 :-)
http://metacalculus.com/prose.html
It links to several manuals (most likely including the one you read) and has links to papers about PROSE and it's predecessor, SLANG.
That said, the coincidence that I mentioned is entirely personal. After a hiatus, I am getting my toes wet again in this language that I find quite interesting. Its a whole program analyzed, aggressively inlined, functional, but not purely so, ML like type-checked and type-inferred language with generics that interacts quite effortlessly with C++, without the need for any ffi like library. It has coroutines and preemptive threads.
I think of it as something that does the same to C++ what Scala does to Java or F# does to C#. It is really performant and achieves the speed via aggressive inlining, tail calls, whole program analysis and what can be called opportunistic but indeterminate laziness. I have not quite grokked its model yet, one thing that I want to get a handle on is to be able to reason what triggers garbage collection and what doesn't. According to the author of the language, garbage collection can be mostly avoided, but I am not yet good at this aspect, but its just been a few days that I have actually used the language (as opposed to reading about it).
Its statically typechecked but feels unusually flexible in one aspect: one can move functions and types around the between and across different translation units freely (for refactoring) without the need for forward declarations because the linkage semantics are permutation independent.
The language that I am talking about is Felix http://felix-lang.org/share/src/web/tutorial.fdoc http://felix-lang.org/share/src/web/nutut/intro/intro_index..... It has been discussed on HN a few times and I have mentioned it a few times myself just because I find it really interesting, not because I have a dog in the fight. The language author does frequent HN but very very rarely.
More on topic, it seems like at least in the example given, inlining of functions followed by standard optimization would give good results, and I thought we were pretty decent at inlining. Is that just because the example is so simple, and the point is that that ability needs to be more general?
But I'm not sure all of us would agree which program is beautiful/familiar and which is ugly/foreign...
Edit: I love LISP, but I love math more.
(I had Firefox 20 before. I checked a few websites and didn't notice any speed-up in general.)
PS: of course you can write modular code in JS too, with a complex lib etc (maybe not quite as nice as scheme). Would it be compiled to be as efficient?
Still, other semantics might slow the code down compared to ASM.js, but the heap allocations will be the biggest.
[1]: http://alexey.radul.name/ideas/2013/cleverness-of-compilers/...
I have never hacked compilers myself, so I might be totally off the mark on all of this. And the specific things I've mentioned might be much harder than what they seem at a cursory glance. But this post is more of an inquiry into the matter than a definitive statement about the status quo.
javac's a very dumb compiler.
All the advanced optimizations like Escape Anaysis are applied by the JIT within the JVM.