Why interpreters matter (at least for high level programming languages)
ics.uci.edu
ics.uci.edu
But recently I've started playing with interpreters and have found them very appealing.
There are multiple reasons for this:
- With an interpreter, there are no separate object files. So you only need to slurp the source code and call eval on it. This reduces workflow overhead to zero and should not be underestimated. For example, in a JS-based language for browsers, this means you don't need an extra set of scripts for a native JS environment (like node.js) for creating JS object files.
- With an interpreter, there's no phase separation. In a compiler you're always dealing with expressions, and you can't deal with the values they evaluate to. In an interpreter you have access to both, so you can do a lot more fun crazy things.
- With an interpreter, the implementor keeps more control of programs. It's simply much easier to instrument and manipulate the execution of programs when you are interpreting them than when they run on their own on the target architecture.
- Potentially smaller code sizes. In languages with macros, source code is highly compressed and irredundant - basically all boilerplate has already been stripped away by the programmer. If you compile this code, you have a huge code expansion compared to the source code.
The article is talking about JIT compilers, not AOT compilers. JIT compilers don't need to generate object files. Compilers can provide eval. JITs use more memory at runtime than interpreters, with AOTs there is no runtime overhead from language processing.
> - With an interpreter, there's no phase separation. In a compiler you're always dealing with expressions, and you can't deal with the values they evaluate to.
This is just plain wrong. Interpreters can have phase distinctions. Any language that could be interpreted could just as well be compiled. I don't even really understand your argument here.
> - With an interpreter, the implementor keeps more control of programs.
What does that even mean?
> - Potentially smaller code sizes.
Interpreters that offer macros will have to expand the source code, too, which will use up memory. The difference is that now macro expansion is happening it runtime, which impacts speed. Furthermore, (dynamically-linked) compiled binaries are almost always smaller than their source code.
There are benefits to using interpreters versus compilers of any sort, and vice versa. This article is discussing the drawbacks of JIT compilers in particular.
This is just plain wrong. Interpreters *can* have phase distinctions.
Any language that could be interpreted could just as well be compiled.
It might be clearer to say "interpreters don't have to have phase separations. And that makes some things easier to do than they are with compiled code.For example, I'm working on a Lisp dialect with first-class macros that are syntactically indistinguishable from function calls. Theoretically, yeah, I'm sure I could compile it; I know compilers have been written that handle first-class macros; but I have only the vaguest idea how. But in an interpreter, it's trivial.
> - With an interpreter, the implementor keeps more control of programs.
What does that even mean?
pwpwp said precisely what it means: "It's simply much easier to instrument and manipulate the execution of programs". I.e., as I understand it, it's way easier to build a highly informative debugger into an interpreter than it is to attach a debugger to compiled code.Yeah, compiling first-class macros can be very tricky. I cut them from the compiler for my own Lisp because I didn't see the benefit of having them (beyond them being neat). If you know of some use-cases where first-class macros are useful, I'd love to hear about them. I might change my mind about cutting them.
The thing about first-class macros, though, is that the macro expansion phase is still separate from the execution phase. It's just that both phases now happen at run time rather than having expansion at compile time.
> I.e., as I understand it, it's way easier to build a highly informative debugger into an interpreter than it is to attach a debugger to compiled code.
That makes sense. I couldn't have derived that from the original statement. Thanks.
If you know of some use-cases where first-class macros are useful,
I'd love to hear about them. I might change my mind about cutting them.
I got nothin' in terms of practical use cases. It's mainly a research thing. It annoys me that Lisp function calls look exactly the same as macro calls, but in fact behave differently, and making macros first-class fixes things up so that same look == same behavior. That way you can do crazy stuff like (map (fn (a) (apply or a)) '((#t #t)(#t #f)(#f #t)(#f #f)))
Where normally this would produce an error "or is not a procedure".My interpreter implements both lambdas and macros as syntactic sugar on top of vau expressions, so if I had a good way of compiling vau expressions, then the first-class runtime macros would be automatic. In the interests of efficiency, I'd like some way of doing static analysis to do as much macro expansion as possible at initialization / compile time, but that's dang tricky figuring out which macros will end up bound to which names before runtime.
Is your vau work available publicly? I find this stuff very interesting.
The syntactic sugar for macros isn't pushed to the public repository yet; not till I get a good blog post written up about it. But it's basically parallel to the operation of "wrap" in Kernel- where "wrap" creates a lambda by evaluating the arguments to a vau expression, "macro" evaluates the return value of a vau expression.
I'm doing most the research stuff on the Schrodinger repository, and things that I like will end up eventually on https://github.com/gliese1337/vernal-lang ; I've stopped actively updating that repository for now because I'm contemplating re-implementing the "real" Vernal interpreter (as opposed to the "research-only" Schrodinger interpreter) in Go, so that there's only 1 level of interpretation going on instead of 2.
For your decently coded basic webserving app, the bottleneck is almost never your code. It is the network, databases, etc. And in my experience, the bound on how many simultaneous requests you can serve is generally not your CPU. It is how much memory it takes to have enough processes ready to handle requests. (Admittedly my experience is mostly with Perl, where multi-threaded code is a bad idea. But Python has the GIL, so again I would plan on using more processes instead of heavily multi-threaded code for concurrency.)
Given this, for the kinds of web applications that I have worked on, reducing memory consumption is the thing that most helps scalability.
I've seen that some people are concerned about webapp memory consumption, and that might be very real for CGI scripts (I remember memory being primary in 1999), but working with PHP, Java and ASP.NET, I see code size as a secondary concern. Facebook developed HipHop because CPU time really is the bottleneck with PHP; and I can say that when I need to upgrade a busy PHP box it's nowhere near running out of memory.
Now there are embedded systems, and sometimes there it's worth making radical efforts to shoehorn things. Some people do it for work, some people do it for a hobby, but if I'm programming for fun I've got better things to do than scrutinizing my toolchain to squeeze out everything that wastes memory so I can run Linux in 8MB of RAM.
This isn't a major concern, but I wanted to point out that anyone on a budget will want memory usage to at least be reasonable. It impacts our ability to experiment and play if we are bound by memory constraints.
Memory consumption of code just isn't an issue because my data is at least 10,000x bigger than the code
JavaScriptCore is getting a new fast interpreter that will be the first choice way to run code that hasn't proven hot: http://trac.webkit.org/changeset/108309
That's basically the crux of it. In python/c#/scala, a for loop looks the same whether you use ints, longs, bigInts or complex numbers as the iterator. In java, you have special, relatively compact syntax for the close-to-the-metal types like ints or longs, but using anything higher level suddenly becomes ridiculously verbose. Writing BigIntegers as for-loop iterators is no fun!
Especially if you include non-ANSI stuff like metaobject protocol or environments.
Metaprogramming in Ruby and Python usually takes advantage of these features to add methods to an object at run-time with a DSL (for example attr_accessor in Ruby, belongs_to in Rails), or to intercept method calls at run-time with method_missing. In contrast in Common Lisp or Scheme you'd write a macro that does the same work at compile time. The code that the compiler sees after macro expansion is much saner from a compiler optimization perspective. It means that you do not need profile guided inlining to get reasonable performance (and my personal opinion is that the code is easier to understand too).