---
title: "Benchmarks"
output: arl::arl_html_vignette
pkgdown:
as_is: true
vignette: >
%\VignetteIndexEntry{Benchmarks}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
arl::register_knitr_engine()
# Read benchmark data checked out by `make bench-data` (or `make vignettes`).
bench_json <- tryCatch({
data_file <- "benchmarks/results/data.js"
if (!file.exists(data_file)) data_file <- file.path("..", data_file)
raw <- readLines(data_file, warn = FALSE)
json_text <- sub("^window\\.BENCHMARK_DATA = ", "", paste(raw, collapse = "\n"))
jsonlite::fromJSON(json_text)
}, error = function(e) NULL)
if (!is.null(bench_json)) {
runs <- bench_json$entries$Benchmark
# runs is a data.frame; benches column is a list of data.frames
n_runs <- nrow(runs)
benches <- runs$benches[[n_runs]]
# Also grab the first run for before/after comparisons
if (n_runs > 1) {
first_benches <- runs$benches[[1L]]
} else {
first_benches <- NULL
}
}
has_bench_data <- !is.null(bench_json)
```
This page summarizes Arl's performance characteristics.
For full profiling details and optimization history, see
[`benchmarks/PERFORMANCE.md`](https://github.com/wwbrannon/arl/blob/main/benchmarks/PERFORMANCE.md)
in the repository.
## Pipeline Overview
Every Arl expression passes through five stages:
```
Source → Tokenizer → Parser → Macro Expander → Compiler → R eval() → Result
```
The table below shows where time is spent on each of the example programs
shipped with the package:
```{r pipeline-table, echo = FALSE, results = "asis"}
if (has_bench_data) {
real_names <- c("fibonacci.arl", "quicksort.arl",
"graph-paths.arl", "macro-examples.arl")
components <- c("tokenizer", "parser", "macro", "compile", "r-eval")
comp_labels <- c("Tokenizer", "Parser", "Macro Expander", "Compiler", "R eval()")
comp_vals <- sapply(components, function(comp) {
vapply(real_names, function(wl) {
idx <- which(benches$name == paste0(comp, "/real/", wl))
if (length(idx) == 1) benches$value[idx] else NA_real_
}, numeric(1))
})
totals <- rowSums(comp_vals, na.rm = TRUE)
pct_mat <- 100 * comp_vals / totals
fmt_cell <- function(ms, pct) {
if (is.na(ms)) return("\u2014")
sprintf("%.0f ms (%.0f%%)", ms, pct)
}
rows <- lapply(seq_along(real_names), function(i) {
cells <- vapply(seq_along(components), function(j) {
fmt_cell(comp_vals[i, j], pct_mat[i, j])
}, character(1))
data.frame(
Workload = paste0("`", real_names[i], "`"),
Tokenizer = cells[1],
Parser = cells[2],
`Macro Expander` = cells[3],
Compiler = cells[4],
`R eval()` = cells[5],
check.names = FALSE,
stringsAsFactors = FALSE
)
})
tbl <- do.call(rbind, rows)
knitr::kable(tbl, format = "pipe")
} else {
cat("*Benchmark data not available — rebuild with access to the `gh-pages` branch.*\n")
}
```
The compiler, R eval(), and macro expander together dominate runtime.
The tokenizer and parser are fast by comparison.
## End-to-End Timings
The same workloads measured end-to-end:
```{r e2e-table, echo = FALSE, results = "asis"}
if (has_bench_data) {
e2e_idx <- grepl("^e2e/real/", benches$name)
e2e <- benches[e2e_idx, , drop = FALSE]
e2e$workload <- sub("^e2e/real/", "", e2e$name)
tbl <- data.frame(
Workload = paste0("`", e2e$workload, "`"),
`Execution Time` = sprintf("~%.0f ms", e2e$value),
check.names = FALSE,
stringsAsFactors = FALSE
)
knitr::kable(tbl, format = "pipe")
} else {
cat("*Benchmark data not available — rebuild with access to the `gh-pages` branch.*\n")
}
```
These are end-to-end times (i.e., tokenize + parse + expand + compile +
evaluate), and also include engine startup and module loading.
## Optimizations Applied
Three O(n²) bottlenecks were identified by code inspection and fixed:
1. **Tokenizer string accumulation** — character-by-character `c()` replaced
with a regex-based approach that processes entire tokens in one pass,
eliminating per-character overhead entirely.
2. **Parser list growing** — repeated `c(elements, list(elem))` replaced
with chunked collection. ~9× improvement for large flat lists
(1000 elements: ~60 ms → 6.9 ms).
3. **CPS overhead removed** — the evaluator was converted from
continuation-passing style with trampolines to a compiler that emits
R code evaluated by R's native `eval()`, removing per-expression
closure allocation.
```{r before-after, echo = FALSE, results = "asis"}
if (has_bench_data && !is.null(first_benches)) {
# Compare first run (baseline) to latest run for e2e/real workloads
e2e_names <- benches$name[grepl("^e2e/real/", benches$name)]
rows <- lapply(e2e_names, function(nm) {
before <- first_benches$value[first_benches$name == nm]
after <- benches$value[benches$name == nm]
if (length(before) == 1 && length(after) == 1) {
data.frame(
Workload = paste0("`", sub("^e2e/real/", "", nm), "`"),
Before = sprintf("~%.0f ms", before),
After = sprintf("~%.0f ms", after),
Speedup = sprintf("%.1fx", before / after),
check.names = FALSE,
stringsAsFactors = FALSE
)
}
})
tbl <- do.call(rbind, rows)
if (!is.null(tbl) && nrow(tbl) > 0) {
cat("\nBefore/after comparison (first recorded run vs latest):\n\n")
print(knitr::kable(tbl, format = "pipe"))
}
} else if (!has_bench_data) {
cat("*Benchmark data not available.*\n")
}
```
All fixes preserve correctness (full test suite passes) and significantly
improve performance on most code, as well as preventing worst-case blowups.
## Benchmark Data
Historical benchmark results are stored on the `gh-pages` branch in
`dev/bench/data.js`. You can inspect them with:
```bash
git show gh-pages:dev/bench/data.js
```
Each entry records a benchmark run with commit metadata and per-benchmark
timings. The tables on this page are generated from the latest run.
## Running Benchmarks
From the repository root:
```bash
make bench
```
This runs the full benchmark suite (component-level and end-to-end) and
saves results to `benchmarks/results/`. Individual component benchmarks
are also available:
```bash
# COMPONENT can be tokenizer, parser, macro, compile, r-eval, stdlib, e2e
make bench-component COMPONENT=tokenizer
```
Profiling reports (HTML flame graphs) can be generated with:
```bash
make profile
# View: open benchmarks/profiles/eval-fibonacci.html
```