Skip to content

Commit

Permalink
ra implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
genos committed Dec 11, 2024
1 parent 7396f3b commit f69a8fa
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 1 deletion.
2 changes: 1 addition & 1 deletion _posts/2023-06-02-random-art.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub struct Mul {

I took up this trick after
[learning](https://users.rust-lang.org/t/is-there-a-better-way-to-represent-an-abstract-syntax-tree/9549/4)
more about it from the inimitable [matklad](https://matklad.github.io/).
more about it from the inimitable [`matklad`](https://matklad.github.io/).
As the linked response says, this keeps the size of the base enum smaller.
In the source code, I've got a [property
test](https://github.com/genos/Workbench/blob/main/ra/src/lib.rs#L230-L233)
Expand Down
137 changes: 137 additions & 0 deletions _posts/2024-12-11-random-art-impls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
title: "Different Implementations of a Tiny Programming Language"
layout: post
---

# Random Art, Revisited

In [a previous article]({% post_url 2023-06-02-random-art %}), we played around
with implementing a tiny programming language for generating random art.
In that article and the [accompanying
code](https://github.com/genos/Workbench/tree/main/ra), we implemented a trick
I learned from [this
post](https://users.rust-lang.org/t/is-there-a-better-way-to-represent-an-abstract-syntax-tree/9549/4),
specifically [`matklad`'s](https://matklad.github.io/) response.
In learning more about programming language implementation, I came across [this
wonderful post](https://www.cs.cornell.edu/~asampson/blog/flattening.html) by
Professor Adrian Sampson, which examines the effects of flattening the abstract
syntax tree of a similar little language.
I thought it'd be interesting to perform a similar analysis with our random art
language implementation, so I put together [a Rust
project](https://github.com/genos/Workbench/tree/main/ra-impls) with four
different implementations.

# Implementations

The first implementation is the most basic, where we represent our AST directly:

```rust
pub enum Expr {
X,
Y,
SinPi(Box<Expr>),
CosPi(Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Avg(Box<Expr>, Box<Expr>),
Thresh(Box<Expr>, Box<Expr>, Box<Expr>, Box<Expr>),
}
```

As mentioned [last time]({% post_url 2023-06-02-random-art %}), it can be more
efficient store a single pointer (i.e. `Box`), which my code confusingly calls
the "branch" version (apologies, but I wanted the names to be in alphabetical
order and it was the best I could come up with).
In this version, the main `Expr` looks like

```rust
pub enum Expr {
X,
Y,
SinPi(Box<Expr>),
CosPi(Box<Expr>),
Mul(Box<Mul>),
Avg(Box<Avg>),
Thresh(Box<Thresh>),
}
```

where each of the intermediate structures looks like `struct Mul(Expr, Expr)`, etc.

After that, we move to a flat version similar to the one explored in Dr.
Sampson's [post](https://www.cs.cornell.edu/~asampson/blog/flattening.html).
In this, we go back to the basic structure, wherein we store (potentially)
multiple things in a single `Expr` variant:

```rust
enum Expr {
X,
Y,
SinPi(ExprRef),
CosPi(ExprRef),
Mul(ExprRef, ExprRef),
Avg(ExprRef, ExprRef),
Thresh(ExprRef, ExprRef, ExprRef, ExprRef),
}
```

This time, though, the `ExprRef`s are instead indices, 32 bit unsigned integers, like `struct ExprRef(u32)`.
We then use a pool of expressions, `struct ExprPool(Vec<Expr>)`; during its
construction, every `ExprRef` index points to a valid index into this vector.
This controls memory layout even more tightly than our branch version, though
interpretation still
[uses](https://github.com/genos/Workbench/blob/main/ra-impls/src/flat.rs#L72)
an additional vector of the same length.

The final version follows [this
comment](https://old.reddit.com/r/ProgrammingLanguages/comments/mrifdr/treewalking_interpreters_and_cachelocality/gumsi2v/)
by [Bob Nystrom](https://craftinginterpreters.com/), modifying our flat version
to implement a stack-based bytecode interpreter.
Every subexpression is interpreted before larger expression that uses it, and
the ordering of items is all that matters.
We can eschew any kind of pointing or indexing, and move directly to a stack of individual instructions:

```rust
enum Expr {
X,
Y,
SinPi,
CosPi,
Mul,
Avg,
Thresh,
}
```

These are then stored in order in a `struct ExprPool(Vec<Expr>)` during
construction, and
[interpretation](https://github.com/genos/Workbench/blob/main/ra-impls/src/stack.rs#L66)
uses a single stack.

# Do you even bench(mark)?

For benchmarking, for each version we build up a random expression of "depth"
26 in the same fashion as last post, then evaluate that expression with a
pseudorandomly chosen `x` and `y`.
Reaching for the amazing [`hyperfine`](https://github.com/sharkdp/hyperfine) to
time our various approaches:

<img src="{{ "/public/ra_shell.png" | absolute_url }}" alt="Hyperfine command for generating benchmarks">

We get the following times:

<img src="{{ "/public/ra_bench.png" | absolute_url }}" alt="Plot of benchmark times by command">

Interestingly, there's a large time improvement in moving from the basic to the branch version alone!
From there, we get further improvements moving to the flat and stack versions,
though the savings are less dramatic.

To keep myself honest, I also double-checked that versions yield equivalent
results (at least, up to a certain depth) via some [property-based
tests](https://github.com/genos/Workbench/blob/main/ra-impls/src/main.rs#L44-L70)
with the help of the exceptional [`proptest`
crate](https://github.com/proptest-rs/proptest).

# Coda & Code

As always, it was fun and interesting to take the lessons learned in reading and see their impacts in practice.
Please feel free to check out [the code](https://github.com/genos/Workbench/tree/main/ra-impls) for more details!
Binary file added public/ra_bench.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/ra_shell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f69a8fa

Please sign in to comment.