LLVM Intermediate Representation is better than assembly
idea.popcount.org
idea.popcount.org
-LLVM at the IR level still has platform specific information - calling conventions, vector types, pointer widths, etc.
-what was true then is still true now: http://lists.cs.uiuc.edu/pipermail/llvmdev/2011-October/0437...
-typed assembly languages have existed for a while, no one uses them
Yes. If I were to write IR for a real project, I'd probably keep a few pretty large functions to avoid overhead caused by calling conventions.
Very interesting link. Thank you! Though I find the arguments unconvincing. More data needed...
> typed assembly
Yes. But now _every_ mac user and plenty of other people have a typed assembly compiler on their machines! IR is reasonably popular and quite well documented. I'm not saying it's new, I'm saying it's cool :)
In many of those there is no going back to the original representation, they are one-way.
If you have IR that you can compile to various archs and have it work there, that is a lucky thing in that particular case. But it is not what LLVM IR was designed for nor should that work in general.
[EDIT: caf's post made it clearer. I know what you meant now]
And once you generate that IR, you can't just built it to an arbitrary target, it must be the one it was generated for.
Consider C:
A C int can be 16 bits. Or 32. Or 64. Etc. As long constraints of the relation to the other types is met.
The moment the frontend specifies a primitive type for a field in the struct, that code is incompatible with a whole lot of platforms.
The fronted must choose which specific integer type that "int" in C maps to. At that point, the IR is no longer machine independent - if you pick 32 bit signed ints to represent C "int", your program will not match the C ABI on any platform using 16 bit unsigned int as C "int" and you won't be able to directly make calls to libraries on that platform, for example.
You might not be able to change the C program - it might be using "int" because the libraries it needs to interface to uses "int" in their signature (and handles it appropriately) on the platforms you care about.
int a() { return sizeof(void *); }
Obviously a trivial example, but it's illustrative: the front-end compiler knows a bunch of things about your target and bakes that information into the IL. If you took IL generated by the compiler with "-arch i386" and then compiled the IL using "-arch x86_64" it's quite possible to get a non-working executable.It's possible to carefully write IL that will work on multiple platforms as done in this blog post, but I'm not sure how useful that really is. You're still giving up the exact control that assembly gives you, so I don't know how much better you'd get than clang-produced IR. In other words, if you want "portable assembly language" use C.
Still, its an interesting blog post. It's good to show people how the compiler works behind the curtain.
Really? Where? Which one? The last time I looked, TAL was only a proposal.
Practically speaking, is there a good reason to do this in non-toy software rather than the more-or-less standard practice of using C as an IR? That gives you, in addition to compilation via LLVM, the ability (in principle) to use gcc, icc, et al. (In practice, in many cases people end up using compiler-specific extensions in the generated C, which does impose limitations on portability to other compilers, but that's certainly avoidable.)
I suppose if you are dynamically generating things that need to be compiled as quickly as possible, eliminating an unnecessary compilation step could be a significant advantage. It doesn't seem like such a constraint would apply in most cases, though.
It's also an issue WRT to precise garbage collection, although that's currently about the same with LLVM, which discards the distinction between pointers and ints. But in theory you or someone might enhance LLVM someday.
If you're hobby-hacking a project, then you may as well just target something that already exists, because micro-inefficiencies that matter on the scale of a deployed production platform probably don't matter for your human-resource-constrained hobby project.
If you're designing the next browser language runtime, or a language meant to be used outside of hobby deployments, there's no point in permanently hamstringing your implementation architecture to save a small amount of implementation time within what is already a large and complex problem space.
It's impossible to implement a variety of useful abstractions in portable C -- for example, function trampolines that do not permute register state and do not appear in the callstack after execution.
The only indirect technical justification is in maintaining compatibility with existing browsers, but that falls over pretty fast when you're a browser maker (Mozilla) and refusing to work with another browser maker (Google) on jumping over that compatibility hurdle.
Indirect: Market constraints that drive technical limitations.
When it comes to browsers, Mozilla both creates and exists within market constraints that drive indirect technical reasoning.
Lashing your industry to JS for another 10 years as a bytecode target is clearly not an optimal long-term solution as compared to the current state of the art, but it might make sense as a legacy support mechanism given the indirect technical constraints.
What do you consider the current state of the art?
A future in which we wind up with architectural support for NaCL-style sandboxing intrinsics, in the same way we saw them be developed for VT-(x|d).
Sticking us with JS for another decade and expecting us to compete against better application runtimes? No.
That depends on the target. I selected them because they represent different aspects of the state of the art.
> ... and I would argue that modern JS VMs are competitive with them in many respects
But not all, and in many places, not even close. JS VMs make trade-offs that we don't need if we abandon JS, and on top of it all, you have Mozilla insisting against shared-state multi-threading supported and exposed by the VM, which discards enormously important optimization opportunities) -- and that's just the tip of the iceberg.
Get rid of JS, get rid of Mozilla's self-enforced constraints on the browser, and look very seriously at something like NaCL which blends native execution performance (including being able to drop to SIMD instructions) with security sandboxing.
Let language authors target the low-level machine, and target your high-level general purpose bytecode.
Once you've built a runtime in which I could implement a JS runtime that's as performant as yours, we will no longer be operating in a two-tier universe in which browser makers think that JS is good enough for everyone but themselves.
1. Shared-state multithreading. This has been proposed by various people in the past, and is still being debated. There are people for it and against it in various places. It might happen, if there is consensus to standardize it.
2. SIMD: PNaCl (which I mention since you mention NaCl in that context) does not have SIMD. But of course it can add SIMD, just like Mono and Dart have, and there is a proposal for JS as well, hopefully that will get standardized.
So even those limitations are in principle resolvable. They do depend on standardization, of course, but so would any VM you want to run on the web.
> Let language authors target the low-level machine, and target your high-level general purpose bytecode.
asm.js is meant to get performance similar to a low-level machine. It's already very close to native on many benchmarks.
> Once you've built a runtime in which I could implement a JS runtime that's as performant as yours
I don't follow that. You can't make a fast JS runtime in your previous examples (the JVM, PNaCl, .NET, etc.).
I included ones you could, such a NaCL. And I should probably have also included the native platforms (Apple, MS, Google), because they're the competition, even if their sandboxing or runtime environment isn't what one might call 'state of the art'.
At the end of the day, it's the two-tier universe that bothers me the most. You're content to foist the JS runtime on everyone but yourselves. Once you implement Firefox entirely as an application running in a JS VM, the argument that JS is good enough might carry some weight.
Nonportable native platforms are there, you can write apps for them. They are even the dominant platforms on mobile.
But the web's entire purpose for existing is to be portable. So you do need something like JS or PNaCl or the JVM. And all of those, due to being portable and secure, impose limitations, such as limiting the native operations that you perform. That's unavoidable. But again, if you don't like that, develop directly for a native platform.
So provide fat binaries. Maximize performance on ARM and i386, with fallback to PNaCL for future platforms that are neither.
> They are even the dominant platforms on mobile.
For a good reason. Adopting their strengths while eschewing their weaknesses (proprietary and single-vendor) would benefit the entire industry greatly.
That's fine for most things, but web content is something that we do want to always be accessible.
It is hard to adopt the strengths of native execution using all the lowest-level tweaks specific to one platform, because that inherently limit portability by definition (and often also security).
I share your goals, but don't think there is an obvious better compromise than the one we are all already making on the web.
If W3C and WHATWG were competent and delivered a viable web application platform, we wouldn't be hiring Android and iOS developers now. Mozilla needs to be more open to new technologies like PNaCl that enable the web to be competitive.
> JS was never meant to be a bytecode and likely will never be able to achieve the performance possible with PNaCl.
LLVM IR was also never meant to be a bytecode. But in both the case of JS and LLVM IR, the question is the final result, not the original intention, even if the two are often connected.
> Mozilla needs to be more open to new technologies like PNaCl that enable the web to be competitive.
PNaCl is not even shipped, so it is too early to evaluate it. But the last benchmarks I saw for compilation speed and performance were mixed.
(NaCl is much more proven, but NaCl is not portable.)
Neither was LLVM IR, if you mean "portable bytecode".
Both asm.js and PNaCl are using the technologies in ways they were not originally designed for.
You cannot do it in NaCl, as being able to map pages rwx would break the security model.
You can either do this through a high-level "JIT API" that generates safe machine code from a validated IR (aka PNaCL), or through a fancier version of mprotect() that validates the actual machine code (aka NaCL).
In a wondrous hypothetical future where processors support a NaCL restricted operating mode, you wouldn't even need to validate; just set the processor state to thumb^Wnacl mode, and define a syscall instruction that switches to direct execution (without actually requiring a context switch to the kernel, just flip an execution flag).
This is why NaCL is so damn interesting, and JS/asm.js is not. NaCL has the possibility of turning our design of restricted execution environments on its head, in a very good (for performance) way.
Edit to respond to your edit: although it would be cool to be able to have a "sandboxed mode" that can somehow be switched out of more cheaply than an interrupt, the whole thing seems like a massive hack to me. After all, NaCl does not take advantage of its pseudo-ability to do so: NaCl code runs in its own process and already incurs a context switch whenever it communicates with the browser, so there is no inherent hardware reason NaCl couldn't just run directly under the kernel and have the kernel provide the same level of sandboxing as the NaCl runtime currently does. It's just an issue of getting such an approach to work portably with existing kernels... hardware support might be able to make it easier to get that to work, but it's probably unnecessary.
If we speculate wildly, why not an "asm.js restricted operating mode"? Not saying that's a good idea, but I'm not sure why a NaCl one would be either. Both PNaCl and asm.js should reach pretty much native speed anyhow.
Btw, the L is not capitalized in NaCl.
Because we already had Jazelle, and it sucked, and we learned our lesson. A Jazelle that operates on ASCII-encoded bytecode with traps for unsupported JS constructs? No thanks. :)
> Not saying that's a good idea, but I'm not sure why a NaCl one would be either. Both PNaCl and asm.js should reach pretty much native speed anyhow.
Pretty much native and actually native are very different things.
I've had to sit down and hand-optimize critical paths that would not have been viable otherwise, and would have meant discarding a feature, or introducing a significant impact on usability -- and that's on platforms where similar investments in runtime library optimization were already made by the platform vendor, too.
If we're going to throw away performance, it has to be for a good reason. As someone who doesn't spend all day on platform development (as interesting as it might be), my focus is in providing the best possible user experience.
I'd love to do that on the web, but the web needs to punting throwing away solid technology decisions because of the bad technology decisions made in the early 90s when none of us knew what the hell we were doing.
> Btw, the L is not capitalized in NaCl.
Whoops. Thanks. Now I will look less stupid in front of software engineers AND my chemistry buddies. :)
I agree.
We are losing performance in return for: portability, security and standardization. The web runs everywhere, has a good outlook for continuing to do so (no fat binaries of current archs), and anyone can build a new web browser based on standards.
None of the other options proposed give us portability, secutiy and standardization right now. Perhaps with more work they might, and perhaps JS VMs will get closer to their speed as well. It's good to try to from both sides to improve things, and people are doing so.
I don't think anyone is overlooking some obvious better solution - that fits the requirements - that is before us. PNaCl (NaCl is not portable, so not relevant here) is interesting, just like the JVM and CLR, but they have major hurdles to pass if they want to be standardized, and that effort has not even begun in the case of PNaCl.
The one quasi-counterexample I can think of in that subset of languages is Objective-C which essentially just adds sugar on top of C and can be transformed relatively simply to C (there are library functions you can use to build classes and objects and call methods from scratch, using no actual Objective-C syntax). My understanding is that Objective-C compilers don't go through C as an intermediate, but this is merely to reduce compiling time, not to improve efficiency at runtime. If Objective-C did target C instead, the end result would be identical.
That makes it totally uninteresting to try to meet your constraint.
We pretty much never try to implement a maximally efficient runtime architecture, cost or resources be damned.
By this argument, there's no direct technical reason why we should not spend 20 years hand writing machine code and proving the code sequence optimal for each target architecture.
It's a policy decision, not a technical decision.
Native is faster. A lot faster. And that's what browser apps are competing against.
That's ridiculous. Mozilla is willing to stick their neck out there implementing a mobile phone operating system, but trying to deploy a sane runtime environment to replace JavaScript is just too risky?
Meanwhile, native mobile and desktop keep growing, and growing, and the web as an application platform loses out, because none of the native platform vendors are quibbling about whether they can convince themselves to ever do something different than what they were doing before.
No, the situation makes it a reality; Mozilla doesn't control Apple or Microsoft.
If they're willing to accept that risk, by comparison, how large of a risk is trying to push through a better standardized browser execution environment, with Google's cooperation?
God forbid they succeed, and we finally have a competitive application platform.
You mean, like Google, who continually pushes to do exactly what I've described here, only to be stymied by:
- Apple, who has no reason to support the web as competitive to their native platform.
- Microsoft, same.
- Mozilla, who refuses to consider that there might be a world beyond HTML/CSS/JS because they believe those specific technologies are intrinsic qualities of the web, and thus central to their mission of supporting the web.
Looks like the only people I disagree with are the Mozilla camp. Microsoft and Apple have different priorities, and Google is continually frustrated by exactly what I've described here.
Reality. Imagine that.
Imagine that.
Mobile is growing hand-over-fist and web apps developers are still arguing about whether they're fast enough to compete, and whether we could possibly maybe actually move past HTML/CSS/JS sometime in this decade.
It's ridiculous, and you claim it's "reality". Fine, your reality sucks, and there's no inherent reason why it has to win ... and it might not.
Mobile has gone from a side-show farmed out to consulting organizations to a mainstream in-house development effort, and the organizations themselves have shifted management and priorities accordingly.
It used to be that almost everyone had a web engineering organization in-house, even non-technology companies. That is changing. Companies like the NYTimes have gone from being grossly unable to manage mobile efforts and farming their work out to subpar contractors, to straight-up building a top-quality team of mobile developers.
Here's the tricky thing about that, too. Those developers, by the nature of where they work in the technology stack, are already quite versatile, and can choose technology solutions outside of the web stack. The problem that most organizations faced originally was that their web departments were a mono-culture and couldn't adapt.
So now you have companies that can and are building technology outside the web, and that means that the network effects that existed before are being torn down. The web tried to leap onto the application bandwagon, and the web failed. Now other technologies are taking over that space.
Who are these development organizations who truly enjoy maintaining five different apps for all the major desktop and mobile platforms and who are aren't going to make the jump as soon as HTML5 delivers everything they need?
Obviously there will always be applications (antivirus, encryption, etc.) for which a browser is poorly suited. But this percentage will never be bigger than it is today.
When will that be, exactly? The promise has been a long time coming, and in the meantime, the constitution of the industry is shifting away from a web myopia.
Next Thursday, 7:39 PM PDT time. Go outside and look at the sky.
> The promise has been a long time coming,
Indeed.
> and in the meantime, the constitution of the industry is shifting away from a web myopia.
I'm sure there are individual companies that fit that description, but I don't see it from where I sit. Here's an example of one of the classic big native platform apps doing stuff on the web. http://office.microsoft.com/en-us/
> the reality is that proprietary application platforms are taking over the application market
That doesn't even make sense as proprietary application platforms have always owned the application market.
You're asserting that there hasn't been a significant management and hiring shift in engineering departments over the past 5 years, moving away from what became a web monoculture in the post-90s environment, from roughly 2000-2005?
> That doesn't even make sense as proprietary application platforms have always owned the application market.
So you admit the web is ill-suited to serve as an application platform, and is failing to acquire traction in that space despite considerable but ill-focused efforts to the contrary?
> You're asserting that there hasn't been a significant management and hiring shift in engineering departments over the past 5 years, moving away from what became a web monoculture in the post-90s environment, from roughly 2000-2005?
I asserted no such thing, learn to read.
> So you admit the web is ill-suited to serve as an application platform, and is failing to acquire traction in that space despite considerable but ill-focused efforts to the contrary?
I admit? What kind of stupid opening is that? Show me where I said anything about the web serving as a great application platform over native apps.
You know what, never mind; you're an argumentative ill tempered child who doesn't know how to have a discussion properly. Have a nice day.
One of us has stuck to discussing the topic, the other, discussing the person. Good day.
There are far far worse systems for evolving widely used platforms.
That's fine, but perhaps it would behoove Mozilla to not participate in dooming the web as a competitive application platform simply due to a misguided belief that the web is defined by HTML/CSS/JS?
The problem with HTML5 is it was designed by committee for displaying documents not dynamic apps. While JS has come a long way to closing the gap with native performance, it's the other HLML5 tech that's holding the web back. CSS is ill-suited to be hardware acceleration and DOM is a performance sucking hack that kills the UX on mobile platforms.
Yes, really.
It's time to move on.
Neither NaCL, PNaCL, nor asm.js are 3rd-party browser plugins.
I agree that it's time to move on. It's time to treat the web as a real application platform, instead of as document model with a JavaScript scripting interface.
I think asm.js is brilliant and I hope it does turn out to be the way forward. But it's also a poster child for maintaining compatibility with existing browsers through standard Javascript.
I have to disagree with that. There are lots of languages whose purpose is to produce JS. For example, it would be very silly for CoffeeScript to use LLVM IR. It would end up reïmplementing all of the things that it shares with JS (that is, most of JS) with no conceivable benefit. Maybe if/when PNaCl is everywhere, it would make sense for it to do that, but even then it's a hard sell as long as four giant companies are competing their asses off to optimize JS.
But I do agree with you if the language doesn't correspond well with JS. In that case, it's probably better to target LLVM and then compile to JS via emscripten.
How does one do call/cc in LLVM, or delimited continuations, for that matter?
And no, getcontext() and setcontext() aren't portable.
LLVM is the lowest common denominator. I'm really not fond of lowest common denominators. (Not to mention the fact that it's bloatware in the first place - it's about twice the size of my favourite compiler, and that's before you add the actual language you're trying to compile.)
Because you literally cannot implement it safely or correctly from the C level on Mac OS X, and any other platform with similar thread/stack/thread-state constraints.
Unless that's a platform you think "nobody uses" ...
1. In theory. But what is that "more suitable and expressive IR/bytecode"? I would argue such a bytecode should be portable, but LLVM IR is not that.
2. There are lots of reasons to target C. C can be compiled by many compilers, while LLVM IR can only be compiled by LLVM to things LLVM can compile to. For example, game consoles, various new embedded platforms, etc. - they all have C compilers, but LLVM might not support them (and possibly cannot support them).
Is the point to produce something optimal for users, or produce something optimal for developers? Most successful game and application platforms trend towards the former, whereas your work on web technologies trends towards the latter.
As for portability, while you're right that a number of things can't be implemented in portable C, you gain the benefit that C has been ported to far more systems than LLVM - there are C compilers even for the Commodore 64.
I agree with you that C is heavyweight, but LLVM is too. Personally I prefer to build code generators directly - the LLVM infrastructure may be great, but I much prefer to write compiler that are self contained and bootstrapped in the language they are intended to compile. Then again I might just be difficult.
Well what you're describing is the whole living purpose of LLVM as a compiler framework. In other words, clang, LDC, GHC, and all other LLVM front-end compilers are non-toy software that transform their respective language into IR and hand it to libLLVM* for optimization/codegen/etc. Targeting C puts a relatively large layer and potentially good deal of ambiguity in between your compiler and the machine code, rather it seems more fitting for experimental or "toy" software to use that in lieu of a real compiler or LLVM frontend.
Well, I'm still plugging away at the Deca compiler that outputs to LLVM IR directly. It's loads of fun!
Practically speaking, is there a good reason to do this in non-toy software rather than the more-or-less standard practice of using C as an IR?
I can tell you why I use it:
* Even through the C interface, I have an actual library API that outputs the IR code directly rather than having to transform my syntax trees into C syntax trees and then output through a C parser library.
* You recursively walk your syntax tree passing an LLVMInstructionBuilder* around to different instruction functions in the API. The result is that your code is a recursive tree-walk (rather than an ordered tree-walk), and the builder object serializes it all for you (including using one instruction's output as input to another) according to the natural order of evaluation in your higher-level language.
Sure, you can write SIMD in LLVM IR, but to do it efficiently you need some knowledge of the target. Should you use i32, or rework your code to use more i16, or throw in some parallel i64 multiplications? Those aren't really portable choices, unless you have a really good auto-vectorizer, in which case the advantage of writing in asm at all disappears, since you can just rely on auto-vectorized C.
That said, for most programmers, most of the time: use intrinsics and let the compiler handle register allocation.
The specific problem he referred to was the heuristics failing spectacularly in high pressure.
In SSA form, they have no need to use heuristics, and if they don't want to, don't. They can optimally choose registers.
This will not fail.
Yes, there are other parts to register allocation. The most common NP complete part referred to, and referred to by the parent (AFAICT), was register choosing. This is not NP complete to do optimally in compilers using SSA form. Period.
There may be other parts to the register allocation pass that use heuristics, and fall down, but most of those are not NP complete on SSA either.
spill free phi elimination in polynomial time :)
There are other algorithms that may coalesce more phis, and can be done in linear time, but do not have such guarantee.
If your concern is dead phis, all dead phis (where dead is defined as unused, or only used by dead instructions) can be eliminated in linear time by performing a single linear time backwards-DCE pass on the graph. This will get all phis and instructions that are dead, or only used by instructions that are themselves dead.
If your concern is useless phis and instructions (for lack of a better term, there are papers, and they use the word "dead" differently, so i'm defining a term here for you), where you define useless as "side-effects do not matter", such that in
*a = 5
c = a
*a = 5
c = a
the second store and assignment is useless,This type of elimination is O(N^3) normally, and unproven bounds in SSA (AFAIK).
You must not mean O(1) because you at least must be walking the instructions to eliminate them (looking at your code, you do).
If you include partial dead store elimination, it will be N^3 at least on non-SSA. see http://www.cs.ucr.edu/~gupta/teaching/201-12/Papers/pde.pdf and friends
Looking at your code, libfirm's DCE the standard SSA backwards DCE algorithm without control dependence computation to eliminate dead phis. It will be O(n).
The load store optimization looks like a modification of stuff i helped Thomas VanDrunen with. It is O(N^2), but will miss loads/stores depending entirely on the strength of your memory dependence calculation.
In fact, it will miss a lot, particularly around loop dependent store/load values that are equivalent.
It will catch my particular example, but GCC's testsuite has load/store examples (see gcc/testsuite/gcc.dg/tree-ssa) that it should fail at without help from other phase ordering/etc.
Dead code elimination technically [0] is O(n). However, it should be called "copying garbage collection" instead.
LibFirms SSA construction is minimal [1], it does not create dead phis. Or course, phis might die due to optimizations, in which case they become unreachable and get garbage collected at some point (see above). However, Firm does not model memory (global variables) as SSA, only registers (local variables).
[0] https://github.com/MatzeB/libfirm/blob/master/ir/opt/dead_co... [1] https://pp.info.uni-karlsruhe.de/publication.php?id=braun13c...
This is why things like loop-closed ssa (http://gcc.gnu.org/onlinedocs/gccint/LCSSA.html) exist, and phi nodes are generated even if the variable is never initially used outside the loop (makes sinking easier)
[0] https://pp.info.uni-karlsruhe.de/publication.php?id=braun13c...
Your time bounds and what you get depends on what you mean by "dead code", and how you perform it.
The vast majority of DCE algorithms, in books, in compilers, etc, on SSA form, have the same properties:
1. The "bad" ones are forward DCE algorithms, and do not eliminate everything they could because they process in the wrong order.
2. The "good" ones, used in most compilers, are backwards DCE algorithms, and eliminate all dead scalar code in a straight line.
Both are O(n).
The standard backwards DCE algorithm has a number of deficiencies, that cannot be eliminated sanely in O(n) time.
1. These DCE algorithms do not consider memory affecting statements dead unless they can prove they are non-side effecting. Proving that is going to usually require at least some N^2 analysis somewhere. This means they miss all dead scalar code that results from a useless memory reload (second order effects), and generally will not eliminate any dead stores. Even though this is code, and it's dead, it is not eliminated.
2. These DCE algorithms that are O(n) don't deal with control altering statements. Jumps to empty blocks, as well as code contained in unreachable blocks, will be considered non-dead unless you include control dependence and some form of branch evaluation. You can compute control dependence in O(n) time. In practice, the constant sucks :) It is also not possible to statically evaluate all branches in O(1) time, so even in the absence of memory statements you cannot guarantee you eliminate all dead code in O(n), only some of it.
3. These DCE algorithms do not handle partially dead code (that is, code that is dead if moved to a different place in the program, but does not otherwise affect semantics).
The canonical example is:
y = a + b
if (...) {
y = c + d
} else {
...
}
The first statement is partially dead (dead along some branches), and performing assignment sinking will make it fully dead (if you sink a copy into the else branch, the original is now fully dead).Performing this type of elimination is N^3 or worse, i haven't seen better time bounds on SSA.
(It is not, AFAIK, completely subsumed by something like SSUPRE)
GCC (and I think LLVM now) actually performs the most trivial case of this, which catches a very large number of cases once other passes are run.
Well, actually, looking again, it looks like over the years it's grown.
I guess nobody's noticed it's basically equivalent to Cliff Click's GCM algorithm at this point, without the nice guarantees.
It looks like it could be trivially modified into the GCM algorithm now.
Time to go file a bug.
1. I would never consider a store dead in the sense of DCE. Those side-effect analysis should be handled elsewhere.
2. DCE should not alter control-flow.
3. Partial dead code elimination is not about dead code or elimination anything. Click understood it correctly, it is about code motion.
Effectively, I argue to push the problems somewhere else. This means that DCE is a very cheap optimization and can be run multiple times during compilation. This somewhat helps with the phase ordering problem.
Partial dead code elimination (and partial redundant code elimination) is still an open research question. There are multiple overlapping approaches, but none subsumes everything.
This eventually led to horrendous compile times (that LLVM was much better at) because passes refused to clean up after themselves at all. After all, cheap DCE/cheap GVN/whatever would just get run later, who cared. Those passes became necessary, and even when cheap, if you run DCE 5 times, it starts to add up. Same with not touching control flow during DCE. Leave it to the CFG simplifier, which now gets run 5 or 6 times.
These are all tradeoffs, and especially in a production compiler, you have to make these tradeoffs sometimes, and do more than is academic-wise ideal to do in a single pass.
This is why LLVM's GVN prunes dead instructions, etc
[0] https://pp.info.uni-karlsruhe.de/publication.php?id=hack06cc [1] https://pp.info.uni-karlsruhe.de/publication.php?id=buchwald... [2] https://pp.info.uni-karlsruhe.de/publication.php?id=braun10c...
Of course, I suspect if you pass luajit an unusual program (say, an unusual distribution of instructions), it would actually perform worse. Register allocation is np-complete, so ultimately with modern programs it's dependent on really good heuristics, and C wasn't written to be a bytecode evaluator.
I think that's unlikely. The LuaJIT interpreter keeps all important state in registers; this is not affected by the sequencing of bytecodes.
Except on register-starved architectures like, say, i686.
2. This is just a bad compiler then. Good static profiling is actually quite good, and quite accurate, in most cases. This includes value and other forms of profiling, rather than just simple static edge estimation based on heuristics. Usually, the thing that gets hurt is not spill placement, but bad inlining decisions.
For diamond shaped switch statement interpreter loops with simple bodies, the real issue is that most greedy/linear allocators are not great at live range splitting. Compilers like LLVM (and to some degree, GCC), move all the variable allocations up to the beginning of the function to make life easy by removing scopes (otherwise you have really really crazy edge cases performing hoisting/sinking optimizations), and then for those that don't get mem2reg'd, can't prove they aren't live all at the same time during the switch due to the loop.
Then they make bad choices about which of these variables should stay in registers because their value profiling infrastructures are non-existent.
Proper region analysis, allocation regions, and better optimistic live range splitting would go a long way towards fixing this, but it's not worth it. There is little to no sense in optimizing LLVM for the very uncommon case of interpreter loops (particularly when one of the goals of LLVM is to ... replace interpreters).
So the basic answer is: It's not really a problem anyone cares to solve, not "it's a really hard problem to solve".
Actually, it is not the register allocation itself, which is NP-complete [0]. Avoiding spills and copys is the hard part.
[0] https://pp.info.uni-karlsruhe.de/publication.php?id=buchwald...
As I said, the real issue is that nobody has chosen to optimize for the interpreter case, because it's uncommon and doing so does not help anything else, but comes at great cost.
Choosing the one edge case everyone has said "we don't care about" and saying "see, they suck at everything!" does not seem quite right to me.
For example, it is completely irrelevant to whether register allocation is a problem for tight SIMD intrinsic loops, for example.
At least in GCC (which is what x264 was likely talking about), the issues are GCC's architecture, and not some fundamental "register allocation is hard" issue.
It sounds like you may be referring to MOV elimination by register renaming, which should make the extra moves no more costly than NOP's. I read that Intel post-Ivy Bridge does this, but haven't been able to find any real documentation. Do you know if this is something one can now rely on, or what the limits of this are (number per cycle, size differences, latency)?
3.5.1.13 Zero-Latency MOV Instructions
In processors based on Intel microarchitecture code named Ivy Bridge, a subset of register-to-register move operations are executed in the front end (similar to zeroidioms, see Section 3.5.1.8). This conserves scheduling/execution resources in the out-of-order engine. Most forms of register-to-register MOV instructions can benefit from zero-latency MOV. Example 3-23 list the details of those forms that qualify and a small set that do not.
Example 3-23. Zero-Latency MOV Instructions
MOV instructions latency that can be eliminated
MOV reg32, reg32
MOV reg64, reg64
MOVUPD/MOVAPD xmm, xmm
MOVUPD/MOVAPD ymm, ymm
MOVUPS?MOVAPS xmm, xmm
MOVUPS/MOVAPS ymm, ymm
MOVDQA/MOVDQU xmm, xmm
MOVDQA/MOVDQU ymm, ymm
MOVZX reg32, reg8 (if not AH/BH/CH/DH)
MOVZX reg64, reg8 (if not AH/BH/CH/DH)
MOV instructions latency that cannot be
eliminated MOV reg8, reg8
MOV reg16, reg16
MOVZX reg32, reg8 (if AH/BH/CH/DH)
MOVZX reg64, reg8 (if AH/BH/CH/DH)
MOVSX
http://www.intel.com/content/dam/doc/manual/64-ia-32-archite...To save a large amount of text here, read http://en.wikipedia.org/wiki/Data-flow_analysis
Once you've done this, particularly the section on bitvector problems, realize that most of the properties being computed make no sense earlier than original scope of the variable, and what to do is not always apparent.
Take PRE for example, here's a simple, but crappily devised example: int main(int argc, char argv) { int b; for (int i = 0; i < 50; i++) { int a; a = argc; b = a; } return b; }
(It's crappy because any value numbering/copy prop/etc would take care of this particular example) A standard PRE is going to want to hoist a = argc out of the loop, not b = a;
But, well, it can't, because it's out of scope! How does it know what it should do? Should it move int a out of the loop? It may end up inserting a stack allocation into a hot loop if it just hoists it one scope up!
Should it create a temporary? If you allow it to create new outside-scope temporaries anyway, what's the point of keeping the scopes?
So now you have to do scope understanding and movement anyway.
Plus, in most cases, you need to construct accurate live ranges of variables anyway, and scopes do not always properly represent those. You also have to compute what variables may share registers on your own too.
Non-renaming forms of SSA (IE that don't literally rename the temporaries) are worse here, because you can accidentally overlap live ranges of variables without too much trouble.
In any case, the basic answer is "Scopes buy you very little, and hurt a lot, because every single optimizer has to care a lot about them"
The places where assembly is justified these days tend to be SIMD inner loops where intrinsic use isn't effective (e.g. due to compilers being pretty lame at vector register allocation) and where a lot of gains can be had by bitops level micro-optimization that depend very precisely on the instruction behavior. I would be surprised if writing LLVM IR was actually better than using intrinsics in these cases.
If it were so it would be neat to see it— an example of taking some well optimized multimedia codec library and converting (some of) the SIMD asm into LLVM IR and showing equal performance would be impressive.
- LLVM IR is cool and in many cases it could be beneficial to use it instead of assembler
- If LLVM is bad for any reason, writing an optimizer is relatively simple. And, as opposed to hand crafted optimizations, it does scale.
LLVMContext &Context = getGlobalContext();
SMDiagnostic Err;
Module *Mod = ParseIRFile(argv[1], Err, Context);Good thing, too - I don't even want to think about how buggy and slow compilers would become if random people started jamming passes into them based on nothing but expediency.
opt -load path/to/pass.so < IR.bc
Which emits (hopefully) optimised IR. Neat huh?Also, if you've ever looked at clang's "-emit-llvm" output you will notice that there are some parts of the IR which are hard to be generated by humans, e.g. dgb by sequential number references.
See e.g.: http://llvm.org/docs/SourceLevelDebugging.html#object-lifeti...
BTW, I taught a compiler course where I gave a project that translated a "turtle graphics" functional language into LLVM IR: http://ezekiel.vancouver.wsu.edu/~cs452/projects/turtlecomp/....
I've worked with other (mostly Motorola-based or some sort of embedded) architectures, and while a bit cumbersome to work with, the assembly was clean and understandable.
Back in the days assembly was my primary way of getting things done, in part because C-compilers cost money back then and I barely had money left after buying a computer.
Once the "PC" (and thus Intel) had won the war and I set foot in X86-country, I started looking at the assembly. It took me less than a week to decide to give up assembly forever.
So yeah. It doesn't take much to be better than X86 assembly.
v4si multiply_four(v4si a, v4si b) { return a*b; }
Much more readable, and just as portable. There's a typedef for v4si, of course, but you do that exactly once and then ignore it. The IR produced is identical, and therefore just as portable.The latest source code dates from 2007, and it's only a prototype, but it's a clever idea.
I remain suspicious that IR is the only representation you need if you do want to do the things on the assembly level. I welcome examples of somebody in the know.
The another topic is how often IR is going to change.
Sure. The point is - you can write IR code that multiplies _any_ number of floats and the backend "should" generate reasonable machine code for any architecture.
> Also I don't see the encoding of assumptions of alignment
Yeah, no memory loads there, data passed in xmm0 and xmm1, plenty of simplifications.
> The another topic is how often IR is going to change.
Fair question. I don't know but given the number of projects that currently use it I'm quite sure it'll be well maintained. Also, keep in mind that adapting IR to a future version (if one is created) is still IMO simpler than adapting assembler. Additionally - you can use IR to generate machine code once (for every architecture). That way you won't depend on your users having LLVM installed.
Except that it doesn't. If you write your code with <2 x float> and codegen for SSE, you'll get <4 x float> code, with two elements going unused. It's functionally correct, but you're potentially missing out on half the throughput. If you write your code with <8 x float>, you'll get two registers for each value, but this can create extra register pressure without actually giving you any increased throughput in return.
1. To bootstrap. There's various architecture-specific bootstrap code to load the kernel into memory in linux.git. The project largely depends on GNU as to assemble reliably; llvm-mc doesn't work half as well.
2. To try out new processor extensions. When new instruction mnemonics come out, assembler have to catch up. I'm not sure why anyone would want to use LLVM IR here, because those instructions are architecture-specific anyway.
http://blog.cloudera.com/blog/2013/02/inside-cloudera-impala...
Before that it was to program a pair of limited range timers to run with slightly different periods so I could lazily read them from C, then by examining their phase and values determine if I got an uninterrupted pair of data, and if so, how many times the timers had rolled over, thus implementing a single, high resolution, extended range timer.
It is also used to exploit processor instructions which do not yet have compiler support.
Also, performance critical code where you feel you can do a better job than the compiler (good luck with that nowadays).
In the first case I feel LLMV's IR wouldn't offer any significant advantage over inline ASM in some C, in the latter I'm quite perplex. Writing very fast assembly usually means targeting a very specific architecture and use tips and tricks that will make your code run faster than what the compiler might do, in this case I fail to see how that would work "portably" with this intermediate language.
So yeah, I don't really see what writing in this language offers over writing some plain old C. I can't really see it replace ASM for... well anything really.
Partially my advantage comes from the fact that I have more knowledge of microarchitecture than compilers do, but the real key is that I am hired to do a very different job than the compiler is: if your compiler took a day (or even an hour) to compile a small program, you simply wouldn’t use it, no matter how good the resulting code was. On the other hand, for a library function with significant impact on system performance, I can easily justify spending a week “compiling” it to be as efficient as possible.
Have you used the Intel SDE? Do you know how helpful the -mix histogram is or isn't? Or just general docs on usage?
I'd like one also. Lacking that, have you found any tools close enough to this to be useful? Intel's IACA is better than pen and paper, but rarely replaces it. Is there an AMD equivalent? Are PTLsim or MARSSx86 useful? (sorry for presuming x86 if not what you work on)
Some research has been moving in that direction, since there does seem to be a demand for it. After all, if someone is willing to wait for you to spend a week hand-tuning a function, maybe they'd also be willing to run a compiler in a "take 10 hours to crunch on this" mode. Example: http://blog.regehr.org/archives/923
That said, it’s also worth keeping in mind the enormous differences in how computers and expert humans currently approach the problem closely parallel the differences in how computers and expert humans play chess; quickly evaluate billions of possible “moves” vs. quickly identify the few most promising “moves” and then slowly evaluate them to pick the best. I fully expect to be regularly beaten by the compiler “someday”, but (a) I believe that day is still several years off and (b) even then, I expect that expert human + compiler will beat compiler alone, just as in chess.
I would argue that for most programmers, they can't beat the compiler; they probably don't have your skills and experience. I'm glad there are still people like you (I've probably used some of your code in embedded projects using VxWorks/PPC), but the truth is that many people like you are writing the compilers (or libraries, as you are). That, and the fact that when you mention profiling, you often get blank stares, plus mentioning algorithmic complexity gets you knotted brows, tends to lead me to believe that the great mass of programmers shouldn't try optimizing, prematurely or otherwise, especially in assembly. Play with it, learn about your whole stack, top to bottom, sure, but very few (such as yourself) can beat a good optimizing compiler.
Practical example: objc_msgSend (which is called for every message send in an Obj-C program) is written in assembly so it can jump to the target method implementation without disturbing any caller-save/callee-save register state: http://www.friday.com/bbum/2009/12/18/objc_msgsend-part-1-th... (and also presumably because C won't guarantee that a tail call is actually compiled as a jump).
Assembly is more relevant today than it has been in awhile, largely because Intel has gone pretty gung-ho with vector instructions that compilers mostly can't figure out how to emit on their own.
For example, say I want to convert an array of bytes into a bitmask such that every zero-valued byte is converted into a set bit. On a 64-bit system, you can do this 64-bytes at a time:
unsigned long bits = 0;
for(unsigned char i = 0; i < 64; ++i) {
bits |= ((unsigned long)(bytes[i] == 0) << i);
}
This isn't a particularly efficient use of the CPU. If you've got a CPU that supports AVX2, you've got two very powerful instructions available to you: VPCMPEQB and VPMOVMSKB. VPCMPEQB will compare two YMM registers for equality at a byte granularity and for every byte in which the registers are equal, will set the destination register to all ones. VPMOVMSKB will take the high bit of each byte of a YMM register and store the result in a GPR. Since YMM registers are 256 bits (32 bytes), you can reduce the entire loop above into just a handful of instructions: two vector loads, two pairs of VPCMPEQB (against zero) and VMOVMSKB, and an OR. Instead of a loop processing data a byte at a time, you can have straight-line code processing data 32 bytes at a time.A 4th generation Core CPU (Haswell) has a tremendous amount of bandwidth. It can do (2) 32-byte loads per clock cycle, 2 32-way byte comparisons per clock cycle, etc. If you're writing regular C code, dealing with 8-byte longs or 4-byte ints, you're leaving much of that bandwidth on the table.
This has yielded (ballpark) 2x-5x improvements to runtime performance; some operations essentially become 'free' from the application perspective whereas they took a significant hit previously and could cause UI stuttering and/or significant CPU burn (which also directly correlates to battery life consumption).
Assembly is far from dead in desktop/mobile development.
People hacking at a very low level, e.g., for boot code, or TLB miss handlers.
Places where you need to be highly efficient, and where cycles matter.
Places where compilers don't operate well (e.g., specialized instructions for doing shared memory operations, or stack manipulations for doing things like coroutines).
I might go a year or so without touching assembly now, but not much more than that.
- Whoever wants to produce the fastest CPU-bound libraries or routines which are to be used in some native language (like the guys who produce the drivers for graphics cards). I've made some such routines.
Assembly is still the only way to reach the limits and who doesn't have to worry about that stuff, good for him. But there are people who do, I'm one of them.
"A major strength of LLVM is its versatility, flexibility, and reusability, which is why it is being used for such a wide variety of different tasks: everything from doing light-weight JIT compiles of embedded languages like Lua to compiling Fortran code for massive super computers."
As I wrote assembly code for x86, I didn't even use assembler, just the inline functionality of the compiler. Using assembler is adding one more dependency in your project. Often it is undesired. Regarding LuaJIT, there other aspects, also important.
Regarding LuaJIT, it compiles in about 10 seconds on a modern computer and does not use LLVM. A lot of it is written with an inline assembly preprocessor called DynASM:
I think he was trying to say "Mike Pall doesn't use LLVM-IR for LuaJIT's bytecode representation of Lua programs, nor does it just embed LLVM processor backends for JIT compilation, for good reason."
Assembler has real world users, but also it has a niche in academic community.
Another very important one is for security. In big companies and governments people use debuggers like IDA Pro for controlling what really executes in the computer in order to protect communications and such from snooping of other companies or governments, for example.
http://blog.cloudera.com/blog/2013/02/inside-cloudera-impala...