So, Google has released a new programming language, which turns out to be a slightly cleaned up version of Algol-68. While depressing, this doesn’t come as a large shock to me- if your prime requirement for a new language is that it doesn’t force you to learn something new, as it seems to be for most programmers, than the ability of the industry as a whole to move forward is limited at best. Radical advances require radical changes.
I’ve taken a couple of swings at writing about Go before this, but nothing stuck. Most everything I had to say I’ve said before, and when even I am bored with my ranting, it’s time to change the subject or shut up. But I’m left with one outstanding question I’d like an answer to, before I move to blissfully ignoring Go:
Why is it that still, today, it’s considered an advantage if not a requirement for a new language to be capable of writing an operating system?
I spent ten years doing device drivers, BIOSs, and embedded programming (before switching to a level where there is a computer under all these layers of abstraction somewhere, but I don’t have to touch it :-). And for all the warts C has, it’s not going to get displaced easily. To displace C, you have to be enough better than C that it’s worthwhile rewriting all the C code that’s out there. This can happen- but the easiest way I see it happening is some language combining C and VHDL. Being able to have the same code as both software (for cheap, simple hardware) or hardware (for performance), that would be something that could displace C in the embedded/OS space. I don’t see how a pure software language can be enough better than C to be worthwhile replacing it.
But whether the OS or embedded people actually adopt the Go language seems to be a moot point- the advantage supposedly is that they could. That because the OS could be written in this language, this some how makes it better for us to write our accounting system or web server in it. Why is that?
Back in the bad old days of MS-DOS, there was a good reason for this. As you didn’t have an OS to speak of, most apps of any size and complexity did end up having to contain what were effectively device drivers. And as computers had neither the memory nor the speed to support high levels of abstraction, a tight coupling between the device drivers and the rest of the app was necessary.
But it’s not the eighties anymore, and hasn’t been for decades. And with the introduction of Windows 3.1 seventeen and a half years ago, applications no longer need to, indeed generally no longer can, include their own device drivers and fiddle directly with hardware. Some overhang was to be expected, but by the mid-1990′s, I expected that we had put the era of bit-twiddling behind us. But with Google’s introduction of Go, it appears that I am- or possibly Google is- sadly mistaken about the demise of the low-level languages.
The one reason I can think of, other than sheer abject stupidity, is performance. Or rather, optimization.
Let me back up here a minute. An optimization is a transformation of the code from a (hopefully) simpler and more abstract implementation to a faster one that does the same thing, generally at the cost of simplicity and abstraction. A simple example of this might be replacing a (slow) divide by a power of two with a (fast) shift right. Note that the optimization is just the transformation of the code- I’m not limiting who, or when, the transformation is applied. More specifically, the optimization may be applied either by the compiler, the run time, or the programmer.
The advantage the bit-twiddling languages like C, C++, and now Go, provide is the ability for the programmer to apply optimizations by hand. This is an advantage to a language designer- all that tedious optimization work can be skipped (notice how proud the Go designers are of compile time- optimizations take time for the compiler to apply). Hand applying optimizations is also an advantage in micro-benchmarks (like the Shootout), and in blog posts and similar. Places where the code size is small enough that the cost, especially in terms of lost abstraction, is negligible.
The problem is that we’re not writing blog posts or micro-benchmarks. We’re writing whole systems. Programs of non-trivial complexity, tens or hundreds of lines of code, possibly even millions. Programs too large for us mere mortals to hold in our heads- at this point abstraction and simplicity are not options, not things that are not nice things to have that we can no longer do without. They are absolute necessities. When our programs are at or near the limits of our comprehension is when the extra complexity of hand optimization can be least afforded- large scale systems programming is the last area that should be doing hand optimization.
The other possibility is for the computer to apply the optimization, either at compile time or run time. In both of these cases, the problem is the same: the computer must
- detect that the optimization can be (profitably) applied, is that the code transformation can be made and does result in (generally) more efficient code,
- determine whether the code transformation can be safely applied, that is applying the transformation doesn’t change the defined semantics of the program, and then
- apply the code transformation.
The tricky part is step #2- basically you have two different programs (one before the transform, and one after), and the compiler has to prove that they behave the same (for some more or less loose definition of what “behave the same” means- see “undefined behavior” in C/C++, for example). It is very easy for step #2 to rapidly become equivalent to the halting problem. And often times the optimization can not be applied without changing the semantics of the program- especially in the face of side effects and exceptions.
Which is why this problem often even hits languages that are not very “bit-twiddly” in nature. The performance hit of not applying the optimization is (or at least can be) very large- and if the compiler doesn’t apply it, the programmer generally will (or at least will want to). For example, because the Ocaml compiler doesn’t apply defunctorization, many in the Ocaml community strongly council avoiding functors altogether, in effect defunctorizing code by hand. Or take this rant (about Google’s Closure)- many of his critiques amount to supposed Java programmers not hand applying optimizations to the source code.
The javascript example is especially relevant to this discussion- as it demonstrates no language avoids the need for performance. You can’t just say “this isn’t a language to do serious computation in, so performance (and thus optimization) doesn’t matter.” If any language could make that claim with a straight face, it would have to be javascript. And yet, despite the supposed unimportance of performance in javascript libraries and programs, not applying by hand certain optimizations is considered a gross violation of acceptable style by experienced javascript programmers. No discussion occurred in the link I supplied above about how much faster the code would be if the hand optimizations were made (or, equivalently, how much slower the code was because the optimizations were not applied). No, the author took the position that because the optimizations were not hand applied, the programmers who wrote it were obviously inexperienced in javascript, and wrote ugly (to an experienced javascript programmer) code.
When hand applying optimizations becomes not a matter of performance, but instead one of style, then what was a problem has metastasized. Abstraction and simplicity are sacrificed even when little or no performance is gained from the sacrifice.
This is why I went ga-ga over Haskell’s super-optimization. It gives us a completely different solution to this problem. It demonstrates that even reasonably large-scale optimizations can be applied- if the language is designed for it. Civilization- and software engineering- advances by increasing the number of things we don’t have to worry about. If you’re designing a language for large scale, system programming, this is the tantamount goal. And the designers of such languages shouldn’t be looking at Algol-68, they should be looking at Haskell.
Why Google doesn’t know this, I don’t know.
Related posts: