Session 28
Optimizing a Simple Interpreter

Where Are We?

You are implementing Boom, a small language for programming with numbers. On Homework 9 and Homework 10, you produce a set of tools for a language with number values, 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 Boom 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.

Then trace the three programs at the bottom of the page.


+++++++.


,[.,]


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

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.

Anita Ward's disco classic Ring My Bell

Before we look at interpreting BF, let's consider a few programs.

First, what do the programs from your exercise compute?

Already things have gotten complex!

Here is 1-through-5, which prints the first five digits in order:

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

This program embodies several key ideas. How does it work?

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

Some things to note about BF programs:

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, so we 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!

Some things to note about the interpreter:

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), 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.

A Preprocessor for BF

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

The idea is quite simple. If character i in the program string contains a '[' or a ']', then jumptable[i] holds the index of the matching bracket. For any non-bracket character in the program, jumptable[i] is 0.

The function compute_jumptable builds the jump table. It does the same thing that the original interpreter does, walking down the string, keeping track of nested brackets, until it finds a match. But the original interpreter does this at run-time, every time it sees a bracket. The optimizing interpreter calls this function once, before interpreting. It records what it finds in an array named jumptable that is the same length as the program.

Note: When building the jump table, the while loop only stops to work 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.

For example:

>>> compute_jumptable('[+++++]')
[6, 0, 0, 0, 0, 0, 0]

>>> compute_jumptable('++>[+++++[<+>>-]<].')
[0, 0, 0, 16, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 9, 0, 3, 0]

With the jump table in hand, the interpreter can now 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 specification you read. Even more better, the code runs much faster!

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.

More to Optimize

There is so much more room to optimize:

  • runs of move instructions and increments/decrements
  • higher-order patterns: copy, inc-and-dec

Runs of moves, increments, and decrements are inefficient. At run time, for each character in the program, the 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.

One way to optimize this process is to parse the program to abstract syntax.

  • Where the concrete syntax has a + operator to increment a slot by 1 and a - operator to decrement a slot by 1, the abstract syntax could have an update(n) operator that adds a positive or negative n to the value in a memory slot.
  • Likewise, it could have a move(n) operator to increment or decrement the pointer by ±n rather than > and < working one slot at a time.

The two new operators would collapse a run of operators in the source code into a single super-operator in the parsed program, allowing the interpreter to treat runs of moves, increments, and decrements with a single pass through the processing loop.

The parser could take advantage of more complex patterns in source programs: loops that zero a position, loops that move a value to another slot, and so on. If we create abstract syntax to fold these patterns into single operators, the interpreter will be even more efficient. Such operators deliver 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) created by programmers 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 interpreter 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