Clojure Gazette 176: The Ultimate Abstraction
The Ultimate Abstraction
Issue 176 - June 6, 2016
Please consider advertising in the Gazette.
Let me recap the little thought experiment we have been setting out. We are exploring the idea that smaller abstractions are, all other things being equal, less risky than large abstractions. And we also saw that, at least in an oversimplified model of code, smaller abstractions can reduce our total code size, despite their overhead. But what about runtime overhead?
In general, calling an abstraction has a runtime cost. Each abstraction is an indirection that can compile to more code than the inline (indirection-free) solution. When functions were first introduced in programming languages, you'd only want to use them rarely. Yes, computers were a lot slower back then. But the main reason not to use them was that compilers were not very good. In order to do a function call, the compiler generated code so save all of the registers, some memory, save the arguments, save the return point, then jump to the start of the function. It was a ton of work just to call a function.
Languages that used a lot of functions, like Lisp, were considered slow. Other languages used JMP instructions (GOTOs) instead. It wasn't until Lambda, the Ultimate GOTO , a paper by Guy Steele and Gerald Sussman, did compiler writers catch on. It turns out that many function calls can be compiled into a single JMP. And many more can be compiled into much tighter code than what was normally done. Those compiler techniques made the early Scheme implementations super fast. They used lots of small functions because they were easier to work with. And those techniques are in good compilers everywhere now.
The runtime costs of abstractions are what keep language implementors up at night. Joe Armstrong, one of the inventors of Erlang, talks about the importance of keeping the memory and runtime overhead of processes and message sends low . Alan Kay talks about how Smalltalk kept the memory overhead of each object down to a single pointer. And one of Rust's "pillars" is "Abstraction without overhead" . Seriously, people are on this. They're working very hard to make it so we can write tiny abstractions.
The lesson in all of this is that when designing a language, we should just pick the most powerful abstraction we can find. Even if the overhead is too high now, with time we can find a way to reduce it. However, it is clear that more robust abstractions are easier to make cheap. Fast function calls are easier to implement than fast dynamic, mutable objects. But if that's what you want, just throw more grad students at the problem.
From the software developer's point of view, it means you should pick a language with fast abstractions. If you're worried about the overhead of
function calls or objects or whatever in your current language, that is a good reason to switch. Your language is keeping you from doing your job. If your job is building abstractions, would you really want to use something that doesn't help you abstract more? We've talked about the code size and runtime overhead. But are there more costs that we haven't considered? We'll look at that next time.