Skip to content
gleam-nativePublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hive

A honeybee colony that showcases the OTP actor stack on the Gleam native target, and benchmarks it against the BEAM — one project, one set of (polyglot) dependencies, two targets.

The showcase (gleam run) walks through:

  • a crew of named actor foragers under a one-for-one supervisor
  • parallel work fanned out to the crew and collected
  • sys.get_state introspection of a running actor
  • a crash (a wasp!) and the supervisor's transparent replacement — the same named subject keeps working, state reset to zero
  • scouts started on demand by a factory supervisor
  • the waggle dance: a message relayed around the crew

Run it on either side:

make run        # native
make run-beam   # BEAM (crash reports appear inline — that's OTP's logger)

Benchmark

make bench

Measures three phases (10 passes each over the supervised crew, fastest of 3 runs, noop-baseline subtracted) on the BEAM, the native JIT, and ahead-of-time compiled native executables (one per benchmark module, via gleam export native -m <module>):

  • forage — Collatz work fanned out to the crew: compute-dominated
  • calls — synchronous request/reply round trips: park + wake per call
  • waggle — a message relayed around the crew: pure send/switch cost

Every configuration must print byte-identical output for every benchmark module, and the AOT executable must reproduce the JIT's showcase output exactly, or the script fails.

Profiling

make profile               # forage phase
make profile PHASE=waggle
make profile PHASE=calls

Exports the phase's profiling harness ahead-of-time (so every Gleam function keeps its gleam$module$function symbol), samples it under macOS's sample, and prints per-function self-time; the full call tree lands in /tmp/hive-profile-<phase>.txt. Every phase's story was written by this loop — fix the top item, re-profile, repeat:

  • forage is ~100% inside forager.flight — the Collatz loop itself, with the runtime invisible (<0.5%). Profiling drove three code generator improvements: inline fast paths for integer / and %, direct self-tail-recursion compiled as intra-function loops, and finally evenness fusion — case n % 2 { 0 -> .. _ -> .. } needs only whether the remainder is zero, which for a tagged small integer is one bit test on the dividend (sign-independent), so the whole remainder computation and the released temporary vanish. Disassembly drove that last one: the % block was 13 instructions, now 4. Together they took the phase from ~93 ms to ~12 ms per pass — past the BEAM.
  • waggle spends its active time on the per-hop message lifecycle. Profiling removed two costs — the actor now caches its per-message selector instead of rebuilding it (which sped the BEAM up even more than native), and the process/name tables use read-write locks — and then drove move-on-send: when a send's message argument is the binding's last use, the code generator hands ownership to the runtime, which moves every exclusively-owned node straight into the receiver's mailbox and deep-copies only shared subtrees. Every waggle hop moves its root. The last copies were the route's subjects, shared with the still-live incoming message — so subjects became atomically counted shared leaves: their count word carries a flag that routes the inline count fast path's cold branch to an atomic instruction, letting sends share the same subject box across process boundaries with no copy and no leak. The finale is receiver-side spine reuse: a clause now frees the deconstructed message container the moment its fields are bound (instead of after the clause body), so the received route's cells reach the handler's list.append uniquely owned — and the native append splices a uniquely-owned left list onto the right in place. Each hop's spine circulates between the processes like a ring buffer: 250,000 of 250,010 appends per run splice with zero allocation, and the phase's remaining active samples are the ownership walk, two small frees, and the scheduler's park/wake — the same relay cost the BEAM pays.

Results

On an Apple Silicon Mac (macOS 14, release-built compiler, Erlang/OTP 29), 2026-08-27:

configuration noop forage/pass calls/pass waggle/pass combined/pass peak RSS
erlang (BEAM) 0.26 s 13.5 ms 13.8 ms 12.6 ms 41.9 ms 84 MB
native (JIT) 0.28 s 12.0 ms 12.6 ms 12.7 ms 40.2 ms 33 MB
native (AOT) 0.01 s 12.7 ms 12.3 ms 12.4 ms 40.5 ms 5 MB

What the numbers say:

  • Native now wins the combined run — 40.2 ms against the BEAM's 41.9, with every phase at parity or ahead.
  • Forage flipped to a native win (12.0 vs 13.5 ms) after starting the project 7x behind: div/rem fast paths, self-tail-call loops, and evenness fusion, each found by profiling and the last by reading the loop's disassembly.
  • Synchronous calls stay a native win — 12.3 ms against the BEAM's 13.8 per pass (~1.2 µs per round trip).
  • The waggle relay sits at parity — 12.4 ms AOT against the BEAM's 12.6 (once 1.9x behind at ~25 ms): selector caching, move-on-send, atomically shared subjects, and early container release plus in-place list.append eliminated every per-hop copy and spine allocation; what remains on both targets is the relay's intrinsic park/wake.
  • Memory and startup are the native target's headline: the ahead-of-time binaries run the full actor workload in 5 MB of peak memory with a 0.01 s startup, against the BEAM's 82 MB and 0.27 s — actors, supervisors, mailboxes and all. The JIT sits between at 32 MB.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages