Skip to content

Commit dab9d01

Browse files
committed
Add portable interpreter benchmarks
1 parent 1ea4c11 commit dab9d01

6 files changed

Lines changed: 256 additions & 0 deletions

File tree

benchmark/portable/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Portable interpreter benchmarks
2+
3+
These workloads exercise ordinary Clojure programs that run unchanged in both
4+
Glojure and let-go. They complement microbenchmarks by covering lazy sequences,
5+
persistent maps and vectors, higher-order collection operations, nested
6+
function calls, and integer-heavy loops.
7+
8+
- `prime-workload.clj`: lazy prime generation and repeated factorization
9+
- `event-analytics.clj`: aggregation of 75,000 event maps
10+
- `game-of-life.clj`: 75 generations on a 48×48 board
11+
- `mandelbrot.clj`: fixed-point Mandelbrot calculation on a 120×72 grid
12+
13+
Build Glojure, then compare it with a let-go executable:
14+
15+
```sh
16+
go build -o /tmp/glj ./cmd/glj
17+
go run ./benchmark/portable \
18+
-glojure /tmp/glj \
19+
-let-go /path/to/lg \
20+
-runs 11
21+
```
22+
23+
The runner warms up both executables, checks that they produce identical
24+
output, alternates which executable runs first, and reports median wall-clock
25+
times. A Glojure/let-go ratio at or below `1.0` means Glojure is at least as
26+
fast for that workload.

benchmark/portable/compare.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"slices"
11+
"time"
12+
)
13+
14+
var workloads = []string{
15+
"prime-workload.clj",
16+
"event-analytics.clj",
17+
"game-of-life.clj",
18+
"mandelbrot.clj",
19+
}
20+
21+
func main() {
22+
glojure := flag.String("glojure", "./bin/glj", "path to the Glojure executable")
23+
letGo := flag.String("let-go", "lg", "path to the let-go executable")
24+
runs := flag.Int("runs", 11, "timed runs per executable and workload")
25+
dir := flag.String("dir", "benchmark/portable", "directory containing the workloads")
26+
flag.Parse()
27+
28+
if *runs < 1 {
29+
fmt.Fprintln(os.Stderr, "-runs must be at least 1")
30+
os.Exit(2)
31+
}
32+
33+
for _, workload := range workloads {
34+
path := filepath.Join(*dir, workload)
35+
glojureOutput, _, err := run(*glojure, path)
36+
check(err, *glojure, path)
37+
letGoOutput, _, err := run(*letGo, path)
38+
check(err, *letGo, path)
39+
if !bytes.Equal(glojureOutput, letGoOutput) {
40+
fmt.Fprintf(os.Stderr, "%s produced different output\n", workload)
41+
fmt.Fprintf(os.Stderr, "Glojure: %q\nlet-go: %q\n", glojureOutput, letGoOutput)
42+
os.Exit(1)
43+
}
44+
45+
glojureTimes := make([]time.Duration, 0, *runs)
46+
letGoTimes := make([]time.Duration, 0, *runs)
47+
for iteration := 0; iteration < *runs; iteration++ {
48+
if iteration%2 == 0 {
49+
glojureTimes = append(glojureTimes, timedRun(*glojure, path))
50+
letGoTimes = append(letGoTimes, timedRun(*letGo, path))
51+
} else {
52+
letGoTimes = append(letGoTimes, timedRun(*letGo, path))
53+
glojureTimes = append(glojureTimes, timedRun(*glojure, path))
54+
}
55+
}
56+
57+
glojureMedian := median(glojureTimes)
58+
letGoMedian := median(letGoTimes)
59+
fmt.Printf(
60+
"%-22s glojure=%-12s let-go=%-12s ratio=%.3f\n",
61+
workload,
62+
glojureMedian,
63+
letGoMedian,
64+
float64(glojureMedian)/float64(letGoMedian),
65+
)
66+
}
67+
}
68+
69+
func timedRun(binary, workload string) time.Duration {
70+
_, elapsed, err := run(binary, workload)
71+
check(err, binary, workload)
72+
return elapsed
73+
}
74+
75+
func run(binary, workload string) ([]byte, time.Duration, error) {
76+
start := time.Now()
77+
output, err := exec.Command(binary, workload).CombinedOutput()
78+
return output, time.Since(start), err
79+
}
80+
81+
func check(err error, binary, workload string) {
82+
if err == nil {
83+
return
84+
}
85+
fmt.Fprintf(os.Stderr, "%s failed while running %s: %v\n", binary, workload, err)
86+
os.Exit(1)
87+
}
88+
89+
func median(values []time.Duration) time.Duration {
90+
sorted := slices.Clone(values)
91+
slices.Sort(sorted)
92+
middle := len(sorted) / 2
93+
if len(sorted)%2 == 1 {
94+
return sorted[middle]
95+
}
96+
return (sorted[middle-1] + sorted[middle]) / 2
97+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
(def services [:api :worker :billing :search :notifications])
2+
(def statuses [200 200 200 201 204 400 404 429 500 503])
3+
(def event-count 75000)
4+
5+
(defn event [i]
6+
{:service (nth services (mod (+ (* i 7) 3) (count services)))
7+
:status (nth statuses (mod (+ (* i 13) (quot i 17)) (count statuses)))
8+
:latency-ms (+ 5 (mod (+ (* i 37) (quot i 11)) 900))
9+
:bytes (+ 200 (mod (* i 7919) 50000))})
10+
11+
(def summary
12+
(reduce
13+
(fn [totals e]
14+
(let [service (:service e)
15+
status (:status e)
16+
failed (if (>= status 400) 1 0)]
17+
(-> totals
18+
(update-in [service :requests] (fnil inc 0))
19+
(update-in [service :failures] (fnil + 0) failed)
20+
(update-in [service :latency-ms] (fnil + 0) (:latency-ms e))
21+
(update-in [service :bytes] (fnil + 0) (:bytes e)))))
22+
{}
23+
(map event (range event-count))))
24+
25+
(println
26+
(mapv
27+
(fn [service]
28+
[service
29+
(get-in summary [service :requests])
30+
(get-in summary [service :failures])
31+
(get-in summary [service :latency-ms])
32+
(get-in summary [service :bytes])])
33+
services))
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
(def width 48)
2+
(def height 48)
3+
(def cell-count (* width height))
4+
5+
(def initial
6+
(mapv (fn [i]
7+
(if (< (mod (+ (* i 17) (* i i 3)) 23) 7) 1 0))
8+
(range cell-count)))
9+
10+
(defn cell [state x y]
11+
(nth state (+ (mod x width) (* width (mod y height)))))
12+
13+
(defn neighbor-count [state i]
14+
(let [x (mod i width)
15+
y (quot i width)]
16+
(+ (cell state (dec x) (dec y))
17+
(cell state x (dec y))
18+
(cell state (inc x) (dec y))
19+
(cell state (dec x) y)
20+
(cell state (inc x) y)
21+
(cell state (dec x) (inc y))
22+
(cell state x (inc y))
23+
(cell state (inc x) (inc y)))))
24+
25+
(defn step [state]
26+
(mapv (fn [i]
27+
(let [alive (nth state i)
28+
n (neighbor-count state i)]
29+
(if (or (= n 3) (and (= alive 1) (= n 2))) 1 0)))
30+
(range cell-count)))
31+
32+
(def final-state
33+
(loop [state initial generation 0]
34+
(if (= generation 75)
35+
state
36+
(recur (step state) (inc generation)))))
37+
38+
(println
39+
(loop [i 0 checksum 0]
40+
(if (= i cell-count)
41+
checksum
42+
(recur (inc i) (+ checksum (* (inc i) (nth final-state i)))))))

benchmark/portable/mandelbrot.clj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(def width 120)
2+
(def height 72)
3+
(def max-iterations 60)
4+
(def scale 1000)
5+
6+
(defn escape-count [cx cy]
7+
(loop [zr 0 zi 0 iteration 0]
8+
(let [zr2 (quot (* zr zr) scale)
9+
zi2 (quot (* zi zi) scale)]
10+
(if (or (= iteration max-iterations)
11+
(> (+ zr2 zi2) (* 4 scale)))
12+
iteration
13+
(recur (+ (- zr2 zi2) cx)
14+
(+ (quot (* 2 zr zi) scale) cy)
15+
(inc iteration))))))
16+
17+
(println
18+
(loop [y 0 checksum 0]
19+
(if (= y height)
20+
checksum
21+
(let [cy (+ -1000 (quot (* y 2000) height))
22+
row-sum
23+
(loop [x 0 total 0]
24+
(if (= x width)
25+
total
26+
(let [cx (+ -2500 (quot (* x 3500) width))]
27+
(recur (inc x) (+ total (escape-count cx cy))))))]
28+
(recur (inc y) (+ checksum (* (inc y) row-sum)))))))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
(defn prime? [n]
2+
(cond
3+
(< n 2) false
4+
(= n 2) true
5+
(even? n) false
6+
:else
7+
(loop [divisor 3]
8+
(cond
9+
(> (* divisor divisor) n) true
10+
(zero? (mod n divisor)) false
11+
:else (recur (+ divisor 2))))))
12+
13+
(def primes (filter prime? (iterate inc 2)))
14+
15+
(defn prime-factors [n]
16+
(loop [remaining n candidates primes factors []]
17+
(let [p (first candidates)]
18+
(cond
19+
(= remaining 1) factors
20+
(zero? (mod remaining p))
21+
(recur (quot remaining p) candidates (conj factors p))
22+
(> (* p p) remaining) (conj factors remaining)
23+
:else (recur remaining (next candidates) factors)))))
24+
25+
(def inputs
26+
(map (fn [i] (+ 1000003 (* i 210))) (range 500)))
27+
28+
(println
29+
[(reduce + 0 (take 2500 primes))
30+
(reduce + 0 (mapcat prime-factors inputs))])

0 commit comments

Comments
 (0)