Tuesday, November 11, 2008

Just In Time compilation vs. the desktop and embedded worlds

Okay, rant mode on. As I was waiting for Eclipse to launch again today, it occured to me that one of the enduring mysteries of Java (and C#/.NET) for me is the continued dominance of just-in-time compilation as a runtime strategy for these languages, wherever they're found. We've all read the articles that claim that Java is "nearly as fast as C++", we also all know that that's a bunch of hooey, particularly with regard to startup time. Of course, if Eclipse wasn't periodically crashing on me with out-of-memory errors, then I'd care less about the startup time - but that's another rant. Back to startup time and JIT compilation...

If you're creating a server-based application, the overhead of the JIT compiler is probably pretty nominal - the first time through the code, it's a little pokey, but after that, it's plenty fast, and you're likely throttled by network I/O or database performance, anyway. And in theory, the JIT compiler can make code that's optimal for your particular hardware, though in practice, device-specific optimizations are pretty minimal.

On the other hand, if you're writing a desktop application (or worse yet, a piece of embedded firmware), then startup time, and first-time through performance, matters. In many cases, it matters rather a lot.

There are a number of advantages to writing code in a "managed", garbage-collected language, especially for desktop applications - memory leaks are much reduced, you eliminate buffer overflows, and there is the whole Java standard library full of useful code that you don't have to write for yourself. I'm willing to put up with many of the disadvantages of Java to gain the productivity and safety advantages. But waiting for the Java interpreter to recompile the same application over and over offends me on some basic level.

On a recent project, we used every trick in the book to speed up our startup time, including a "faked" startup splash screen, lazy initialization of everything we could get away with, etc, etc. Despite all that effort (and unecessary complication in the code base), startup time was still one of the most common complaints from users.

Quite a bit of profiling was done, and in our case, much of the startup time was taken up deep inside the JIT, where there was little we could do about it. Why oh why doesn't anybody make a Java (or .NET) implementation that keeps the safe runtime behavior, and implements a simple all-at-once compilation to high-performance native code? Maybe somebody does, but I haven't heard of them.

For that matter, why don't the reference implementations of these language runtimes just save all that carefully-compiled native code so they can skip all that effort the next time? The .NET framework even has a global cache for shared assemblies. Why those, at least, aren't pre-compiled during installation, I can't even imagine.

Update:
I was helpfully reminded of NGen, which will pre-compile .NET assemblies to native code. I had forgotten about that, since so much of my most recent C# work was hosted on Mono, which does things a bit differently. Mono has an option for AOT (ahead of time) compilation, which works, sort of, but could easily be the subject of another long article.

6 comments:

Anonymous said...

I wonder if one reason they don't cache the compiled code from one session to the next is that the code is compiled and optimized for each individual session. If you use an app in a radically different way from one session to the next, different code would be compiled, and that code might have different optimizations in it.

Unknown said...

You can run your .NET apps through NGEN: http://msdn.microsoft.com/en-us/library/6t9t5wcf%28VS.80%29.aspx to get "native images".

Ben Lings said...

On .net at least, code is often ngen'd when it's installed.

Mark Bessey said...

Ben & Adam, thanks for the reminder on NGen - I had completely forgotten about it.

I still think it's a bit wonky that you need to manually invoke NGen, and that images can get "invalidated" for unspecified reasons, which would require you to regenerate them.

jeff said...

There's gcj, which can compile directly to native machine code on any platform supported by the GNU compiler chain. when I last used it, back in 2005, the library support was a bit limited, but it currently supports most of the 1.4 libraries with some 1.5 additions. I don't know if that support includes GUI stuff (it didn't back in 2005).

Unknown said...

Excelsior JET is a Java SE 6 compliant JVM with an Ahead-Of-Time compiler. There is also a number of embedded Java implementations with AOT compilers, e.g. ARM Jazelle used to include such option.