plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
e7acf3c
— Peter John
2026-11-06T14:51:56+05:30
Simpler language T1: baseline benchmark of master
docs/plans/2026-11-06-simpler-language.benchmark.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Benchmark: baseline on master
|
|
2
|
+
|
|
3
|
+
Taken 2026-11-06 on `master` at 33fe838, before any change of the `simpler-language` branch. The tree is exported to `.superlite/baseline-master/` (`git archive master`, not a worktree) and built there with its own `target/`. Every number below comes from that tree's own binaries:
|
|
4
|
+
|
|
5
|
+
- `.superlite/baseline-master/target/release/plum` (release)
|
|
6
|
+
- `.superlite/baseline-master/compiler/game/web-test/target/release/plum-web-test` (release)
|
|
7
|
+
|
|
8
|
+
Machine: Apple M3 Pro, macOS. Later tasks compare against this file.
|
|
9
|
+
|
|
10
|
+
## How the games were run
|
|
11
|
+
|
|
12
|
+
Command, from the baseline root: `plum-web-test compiler/game/web-test/fixtures/<name>.plum --measure 5 --json`. Headless Chromium with software WebGPU (backend `gpu-webgpu`), 60 fps vsync-capped, about 298 frames measured. The same command and shape as `docs/plans/2026-11-05-game-engine.results.md`.
|
|
13
|
+
|
|
14
|
+
`plum-web-test` runs `cargo build -p plum-cli` and then `target/debug/plum build`, so the game wasm was built by the baseline's debug `plum`. The compiler output does not depend on the build profile. Compile times below use the release `plum`.
|
|
15
|
+
|
|
16
|
+
## Which fixtures draw what
|
|
17
|
+
|
|
18
|
+
Fixtures are in `compiler/game/web-test/fixtures/`.
|
|
19
|
+
|
|
20
|
+
| fixture | scene style | scene |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `stress` | none, immediate drawing on `Game` | A: 5000 sprites, 500 rects, 200 circles, 50 texts. Rects and circles drawn as shapes. |
|
|
23
|
+
| `stress_scene` | imperative (`std/game/imperative/Scene`, actors that embed `Actor`) | A: 5000 sprites in 50 rotating groups, 500 rect actors, 200 circle actors, 50 text actors |
|
|
24
|
+
| `stress_pure_scene` | pure scene graph (`std/game/pure/PureScene`), not imperative and not state-and-view | A, but rects and circles are sprites of a 1x1 white texture |
|
|
25
|
+
| `stress_state_view` | state and view (`std/game/functional/FunctionalScene`) | A as `stress_pure_scene`, plus 200 animated sprites (pulsers) and 20 UI buttons |
|
|
26
|
+
| `stress_state_view_deep` | state and view | like `stress_state_view`, but all 5000 dancers in one list, so `step` recurses 5000 deep |
|
|
27
|
+
| `stress_world` | imperative (`std/game/imperative/Scene`) | B: 20000 sprites in one y-sorted layer, culled to the view. A different scene from all the others. |
|
|
28
|
+
|
|
29
|
+
Answers for the comparison:
|
|
30
|
+
|
|
31
|
+
- Imperative: `stress_scene` and `stress_world`. State and view: `stress_state_view` and `stress_state_view_deep`. `stress_pure_scene` is a third style.
|
|
32
|
+
- Same scene: `stress_scene`, `stress_pure_scene` and `stress_state_view` draw the same 5000 / 500 / 200 / 50 scene (A), but with differences. Pure and state-and-view draw rects and circles as white-texture sprites, not shapes. `stress_state_view` also has 220 extra entities (200 pulsers, 20 buttons).
|
|
33
|
+
- No state-and-view fixture matches `stress_world`.
|
|
34
|
+
- The only exact imperative to state-and-view pair is therefore `stress_scene` against `stress_state_view`, with the two differences above. A fair functional port of `stress_scene` and of `stress_world` has to be written in T14.
|
|
35
|
+
|
|
36
|
+
## Game results
|
|
37
|
+
|
|
38
|
+
Milliseconds per frame, mean over 5 s. Load average is the 1 minute value from `uptime` at the start and end of each run. The machine was busy for the whole session (load 7 to 12), so these are noisier than the 2026-11-05 numbers (load 2 to 5).
|
|
39
|
+
|
|
40
|
+
| fixture | run | load | update | draw | render |
|
|
41
|
+
|---|---|---|---|---|---|
|
|
42
|
+
| stress | 1 | 10.4 to 11.1 | 0.013 | 0.663 | 0.321 |
|
|
43
|
+
| stress | 2 | 11.1 to 11.7 | 0.012 | 0.667 | 0.306 |
|
|
44
|
+
| stress | 3 | 11.7 to 11.7 | 0.011 | 0.678 | 0.322 |
|
|
45
|
+
| stress_scene | 1 | 11.7 to 11.6 | 0.074 | 1.339 | 0.332 |
|
|
46
|
+
| stress_scene | 2 | 11.6 to 10.9 | 0.075 | 1.368 | 0.330 |
|
|
47
|
+
| stress_scene | 3 | 10.9 to 10.1 | 0.072 | 1.362 | 0.339 |
|
|
48
|
+
| stress_world | 1 | 10.1 to 9.0 | 0.573 | 0.621 | 0.143 |
|
|
49
|
+
| stress_world | 2 | 9.0 to 7.4 | 0.630 | 0.635 | 0.157 |
|
|
50
|
+
| stress_world | 3 | 7.4 to 7.4 | 0.636 | 0.699 | 0.143 |
|
|
51
|
+
| stress_pure_scene | 1 | 7.4 to 8.8 | 0.227 | 1.253 | 0.239 |
|
|
52
|
+
| stress_pure_scene | 2 | 8.8 to 8.3 | 0.242 | 1.265 | 0.261 |
|
|
53
|
+
| stress_pure_scene | 3 | 8.3 to 9.1 | 0.244 | 1.243 | 0.245 |
|
|
54
|
+
| stress_state_view | 1 | 9.1 to 10.1 | 0.057 | 1.545 | 0.239 |
|
|
55
|
+
| stress_state_view | 2 | 10.1 to 10.3 | 0.068 | 1.583 | 0.262 |
|
|
56
|
+
| stress_state_view | 3 | 10.3 to 9.4 | 0.074 | 1.593 | 0.264 |
|
|
57
|
+
| stress_state_view_deep | 1 | 9.4 to 9.1 | 0.042 | 1.279 | 0.240 |
|
|
58
|
+
| stress_state_view_deep | 2 | 9.1 to 8.4 | 0.059 | 1.338 | 0.255 |
|
|
59
|
+
| stress_state_view_deep | 3 | 8.4 to 9.0 | 0.052 | 1.300 | 0.261 |
|
|
60
|
+
|
|
61
|
+
Means of the three runs:
|
|
62
|
+
|
|
63
|
+
| fixture | update | draw | render |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| stress | 0.012 | 0.669 | 0.316 |
|
|
66
|
+
| stress_scene | 0.074 | 1.356 | 0.334 |
|
|
67
|
+
| stress_world | 0.613 | 0.652 | 0.148 |
|
|
68
|
+
| stress_pure_scene | 0.238 | 1.254 | 0.248 |
|
|
69
|
+
| stress_state_view | 0.066 | 1.574 | 0.255 |
|
|
70
|
+
| stress_state_view_deep | 0.051 | 1.306 | 0.252 |
|
|
71
|
+
|
|
72
|
+
Noise:
|
|
73
|
+
|
|
74
|
+
- Every run drew 1 call per frame and held 60 fps (59.6 to 60.0), so frame time says nothing here; only the update, draw and render split does.
|
|
75
|
+
- Run to run spread inside one fixture: draw within about 3% (stress_world 12%), render within 5% (stress_world 9%), update within 3% for stress_scene, 10% for stress_world, 7% for stress_pure_scene, and up to 30% for stress_state_view and stress_state_view_deep (values near 0.05 ms, close to the 0.1 ms timer resolution).
|
|
76
|
+
- Against 2026-11-05 (load 2 to 5) draw is 0 to 8% higher and update on stress_scene 25% higher (0.0585 to 0.074), so this load adds a few percent to draw and more to the smallest update times. Treat a difference under about 10% on draw, or under 0.02 ms on update, as noise. Re-run both sides back to back before drawing a conclusion near that size.
|
|
77
|
+
- The final comparison should run baseline and branch alternately, not in two blocks.
|
|
78
|
+
|
|
79
|
+
## Compile time, size, sweep, code size
|
|
80
|
+
|
|
81
|
+
Load 8.4 at the start. Release `plum compile`.
|
|
82
|
+
|
|
83
|
+
| item | value |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `stress_scene.plum` compile, 3 runs | 0.54, 0.52, 0.53 s |
|
|
86
|
+
| `.wasm` size | stress 34249, stress_scene 93014, stress_world 69024, stress_pure_scene 67122, stress_state_view 62720, stress_state_view_deep 51765 bytes |
|
|
87
|
+
| wasm sha256 (first 16 hex) | stress 1255dce1ae400be7, stress_scene dd7276d2fc05aa49, stress_world e8c3995c33331bd3, stress_pure_scene 7cb024b3efb69703, stress_state_view dd75f435320dad13, stress_state_view_deep 156b109e75574f07 |
|
|
88
|
+
| sweep `plum test std examples tests` | 209/209 files, 961 tests, 0 failed; 25.9 s and 25.0 s wall (2 runs) |
|
|
89
|
+
| `std/` | 9377 lines in 40 `.plum` files |
|
|
90
|
+
| `compiler/` Rust | 52311 lines in all `.rs` files (35811 of them under `src/`), excluding `target/` |
|
|
91
|
+
|
|
92
|
+
`plum compile` has no `--no-fusion`; only `run` and `build` do, so no no-fusion size was taken.
|
|
93
|
+
|
|
94
|
+
## Loop probes
|
|
95
|
+
|
|
96
|
+
File: `scratchpad/loops_fixed.plum`, a copy of `loops2.plum` with the timer arguments the right way round (`t1.since(t0)`), so times are positive. 50,000,000 iterations each; `plum run` (wasmtime), release `plum`. Load average at the runs: 10.4 (1 minute).
|
|
97
|
+
|
|
98
|
+
Five probes: `for` with a `total =` accumulator, `while`, a tail-recursive `foldTo` with a closure, `repeatUntil` over a struct state, and `findFirst` (pure, returns `Option[Int]`).
|
|
99
|
+
|
|
100
|
+
| probe | default, 3 runs (ms) | `--no-fusion`, 3 runs (ms) | answer |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| for | 13, 14, 14 | 15, 14, 14 | 2499999950000000 |
|
|
103
|
+
| while | 14, 14, 14 | 13, 15, 14 | same |
|
|
104
|
+
| fold | 14, 14, 14 | 14, 14, 13 | same |
|
|
105
|
+
| until | 14, 14, 14 | 14, 14, 15 | same |
|
|
106
|
+
| find | 0, 0, 0 | 14, 14, 14 | 49999999 |
|
|
107
|
+
|
|
108
|
+
- Under the default build, `find` reads 0 ms because fusion moves the pure `findFirst` call past the timer, to where its answer is printed. The answer is printed in the fixed file, and with `--no-fusion` the call runs at the right place and takes 14 ms, the same as the others. Use the `--no-fusion` figure for `find`.
|
|
109
|
+
- All five forms take 13 to 15 ms for 50 million iterations (about 0.28 ns each), so the compiler flattens `for`, `while`, the closure fold and the struct state loop to the same code on this machine. Timer resolution is 1 ms. The earlier "loops +75%" figure does not show up in these probes.
|
|
110
|
+
- Spread is 1 to 2 ms, which is about 10%; the forms cannot be told apart.
|