Session 28
Optimizing a Simple Interpreter

Where Are We?

You are implementing a small language for programming colors, Huey. On Homework 9 and Homework 10, you produce a set of tools for a language with colors, operators for manipulating them, and local variables. On Homework 11, you will add sequences of statements and mutable data.

Our next two sessions will step away from the Huey interpreter to consider other programming language issues you hear about in modern software development. Today, we consider optimization: techniques that interpreters sometimes perform in order to make programs run faster or use less space. As our sandbox for this discussion, we'll use an odd little language...

A Quick Puzzle: An Odd Little Language

Read the top of this handout, which describes a Turing-complete eight-operator language, and then trace the three programs at the bottom:

+++++++.


,[.,]


++ > +++++ [ < + > - ] < .

If those prove too easy, then try your hand at this one:
  
+++>+++++< 
[>>>+>+<<<<-]>>>>[<<<<+>>>>-]<
[<<
  [>>>+>+<<<<-]>>>>[<<<<+>>>>-]<
  [<<+>>-]
<-]

Unless you are a computational savant, getting through this last one will not be possible in the time we have for the exercise — unless perhaps you recognize some patterns and do batches of operations at a time. Can you get to the end of Line 2?

The BF Language

"Who can program anything useful with it?"
— any programmer, any time, anywhere

The language you just played with is BF. It is:

the ungodly creation of programmer Urban Müller, whose goal was to create a Turing-complete language for which he could write the smallest compiler ever

The name "BF" is an acronym of sorts for the language's actual name, which includes a word I won't use here, or anywhere else, to be honest. I'll call it BF throughout this discussion.

According to Wikipedia, Müller first wrote a compiler that took up only 296 bytes and then later lowered that to 240 bytes. Another enthusiast has written a BF compiler in 104 bytes of assembly language.

We programmers are an odd lot.

BF serves as a useful language for us today. Even though it is tedious to write BF programs, it is a fairly standard programming language, with pointers, dereferencing and loops. It's incredibly simple and can be interpreted by a simple program.

Indeed, BF is almost a theoretical model for how programs work. In your theory of computation course, you might learn that the lambda calculus and the Turing machine are two models for how computation works. BF resembles a Turing machine in the same way that our pure function solution for implementing pairs in Session 22 resembles the lambda calculus.

As simple as they are, BF programs are far less efficient than we might hope at run-time. As a result, the language offers us a small playground for exploring one of the ways that modern interpreters and compilers improve the performance of programs: optimization.

Before we look at interpreting BF, let's consider a few programs. First, the programs from your exercise:

Already things have gotten complex!

Let's step back to a simpler program, 1-through-5, which prints the first five digits in order:

++++++++ ++++++++ ++++++++ ++++++++ ++++++++ ++++++++
>+++++
[<+.>-]

and consider how it works:

The code gets long fast, but we can write amazing programs in BF:

Things to note:

The last of these features enables a "literate programming" style, in which the programmer can intersperse BF code freely among text that describes the code. This version of 2 plus 5 has running commentary on the right!

Another cool thing to note: we can translate BF code directly into C using this set of formulas:

... assuming, of course, that we create a main function to contain the code, define p as a char*, and create the memory array first.

(Go ahead, try it: Write a program that takes in a BF program and produces the equivalent C!)

A BF Interpreter

To understand the language better and have a starting point for improving performance, consider this simple BF interpreter written in Python.

We don't have Racket reading Racket-friendly input here, but we still have a simple parser. BF's concrete syntax is sparse: only eight characters, ignoring all others!

In the interpreter itself, six of the operators can be implemented as one-liners.

The more challenging cases are the branching operators, [ and ], which pair up. When the interpreter figures out that it needs to jump, it has to find the matching bracket, forward or backward. Because brackets can be nested, finding the matching bracket requires a bit of bookkeeping. If this seems to you to be a wasteful thing to do at run-time, you are right. It implements an O(n²) operation every time the interpreter tries to jump to the top or bottom of a loop!

We have a something to optimize

Things to note:

Optimizing the Interpreter

"Optimize" is a misnomer... We aren't shooting for a perfect program, only one that is better in some way we care about.

Optimization can be done at any point in the process of processing a program. In our case, we will optimize the parsed program before we pass it to the interpreter.

As we saw above, there is one obvious way to improve my simple interpreter: avoid looking for the matching bracket every time it sees a [ or a ] . The potential speed-up in a serious program, such as a program to factor an integer, is quite large. If there is a "hot" inner loop (one that runs many, many times during one execution of the program, even billions), the simple interpreter will scan the source to find the matching bracket every single time.

An alternative is to precompute the jump destinations before execution. This is safe, because the BF program does not change while it is running.

We can implement this idea in a preprocessor. This interpreter creates a jumptable and passes it to the interpreter.

The function that creates the jumptable does the same thing that the simple interpreter does. But the simple interpreter does it at run-time, every time it sees a bracket, while this function does it once, before interpreting, and records what it finds. The result is an array named jumptable that is the same length as the program. jumptable[i] holds the index of the matching bracket. For any non-bracket character in the program, jumptable[i] is 0. The while loop stops to work only on [s, because it can record the corresponding index for the matching ]s at the same time.

The process is still O(n²), but now the interpreter runs the process only once, before the BF program is executed.

Demo compute_jumptable on these expressions:

Now the interpreter can handle all eight operators quickly: six one-liners and two two-liners. (The two-liners could be one-liners...) Even better, they read just like the descriptions of the operators in the language handout you read. Even more better, the code in our evaluator itself is much simpler now!

The speed-up from this step is noticeable. On a program to factor large numbers, the run-time is now less than half the run-time of the simple approach.

There is so much more room to optimize:

Runs of moves, increments, and decrements are inefficient. For each character in the program, our interpreter must:

  1. advance the pc and compare it to the length of the program,
  2. retrieve the instruction at the pc,
  3. find the right case to execute based on the value of the instruction, and
  4. execute the instruction.

An idea for optimizing this process: parse the program to abstract syntax (!). In our abstract syntax, we could increment (or decrement) the pointer (or the value in a memory slot) by n, rather than 1. This would collapse a run of operators into a single super-operator in the abstract syntax.

There are more patterns to find in source programs and thus more abstract syntax to create: loops that zero a position, loops that move a value, etc.

Implementing these pattern-finding ideas would allow the interpreter to execute even more efficient code. They can give another 40% speed-up.

Comparing Results

In order to compare the execution times of the basic and optimized interpreters, I created versions of both that support a verbose mode: simple and optimized. To run them in verbose mode, pass a -v flag after the BF program name when you run the interpreter.

Esoteric Languages

BF is not unique. There is a family of so-called esoteric languages (also known as esolangs) that programmers have created to test the boundaries of programming language design — or simply to have fun.

BF was not the first esoteric language created, but it is the best-known and the launching point for many others. One of my favorite descendants of BF is Ook. I once wrote an Ook interpeter on a flight home from OOPSLA in California. Here is "Hello, world" in Ook. (You can run the Ook program here.)

One of the more artistic esolangs is Piet, which is named for the Dutch abstract painter Piet Mondrian. He created paintings that look like this:

a Piet program that prints 'Piet'

That is a legal program in the Piet language! It prints 'Piet'. Here is another legal Piet program:

a Piet program that prints 'Hello, World'

It prints "Hello, World". Here's another:

a Piet program that determines if a number is prime

This program is prime? : it reads a number from standard input, determines whether it is prime or not, and prints 'Y' or 'N'. And it does so with a smile! Amazing.

How about this one?

a Piet program that prints 'tetris'

Do you notice anything special about the image? It is made up entirely of legal Tetris pieces. The program prints... "Tetris". Programming truly is an art!

If you don't believe my claims that these are legal Piet programs, try this online Piet interpreter. It uses a simple Javascript user interface in front of a Piet interpreter, the code for which we can download, compile, and run on your computer.

What can we learn from all this?

BF in Racket

Here's one last example of how programmers can have fun with esoteric languages, one that connects back to our studies this semester. In Session 21, we learned how to create new syntax in Racket and that we could even create a whole new language within Racket. Programmer Danny Yoo implemented a Racket language for BF so that we can run BF programs in Dr. Racket!

After we install Yoo's package, we can put a new #lang line at the top of any BF program, load it in to Dr. Racket, and hit 'Run'. Here are Hello, World! and factorial.

Programmers really are a funny lot. Mostly, we just like to program.

References

Language alert: all the pages linked here use BF's full name.

I adapted the description in our opening handout from Brian Raiter's introduction to BF.

The inspiration for this session came from a short blog series by Eli Bendersky on how to write just-in-time compilers. Part 1 of the series discusses BF interpreters and optimization. He covers JIT compilation in Part 2; We don't get to see much of that in this session.

The BF programs I give you come from Raiter, Bendersky, Wikipedia, and Daniel Cristofani.

To run BF programs in Dr. Racket, you have to install the dyoo/bf package. When you open your first BF program in Dr. Racket, hit 'Run' to install and run the code. Thereafter, you'll be able to run BF programs immediately. You can even run them at the command-line:

$ racket hello-world.rkt | more
Hello, World!

Wrap Up