Performance & BEAM Internals

Also: :timer.tc/1, flame graphs via eflambe, :erlang.statistics/1.

43.1 How the BEAM runs your code

One scheduler per CPU core; each runs processes from a run queue.

Preemptive scheduling by reductions (~4000 function calls per slice) → fair latency.

Per-process heaps and per-process GC — no stop-the-world pauses for the whole system.

Binaries > 64 bytes are reference-counted and shared off-heap; small binaries are copied.

Messages are copied between processes (except refc binaries / literals).

JIT (BeamAsm, OTP 24+) compiles BEAM bytecode to native code at load time.

Dirty schedulers for long-running NIFs and I/O.

43.2 Performance tips

Use the right data structure: maps for lookup, tuples for fixed records, ETS for shared reads.

Avoid length/1, ++, and Enum.at/2 in loops (O(n)); prepend and reverse.

Build output with iodata, not string concatenation.

Watch out for large messages between processes (copied) — keep state local.

Beware of sub-binary leaks: a tiny slice can keep a huge binary alive — use :binary.copy/1.

Avoid GenServer bottlenecks: read directly from ETS, partition work.

Use Task.async_stream for parallel I/O-bound work.

Keep mailboxes small; selective receive on huge mailboxes is slow.

Prefer pattern matching in function heads over cond/if chains.

Measure before optimizing.

43.3 Benchmarking & profiling

Also: :timer.tc/1, flame graphs via eflambe, :erlang.statistics/1.

Benchee.run(%{
  "Enum.map" => fn -> Enum.map(1..10_000, &(&1 * 2)) end,
  "for" => fn -> for x <- 1..10_000, do: x * 2 end
}, time: 5, memory_time: 2)
mix profile.eprof -e "MyApp.run()"
mix profile.fprof -e "MyApp.run()"
mix profile.tprof -e "MyApp.run()"   # 1.17+ / OTP 27

43.4 Memory & garbage collection

:erlang.garbage_collect(pid)
Process.flag(:fullsweep_after, 10)
Process.hibernate(M, :f, [])          # compact a mostly idle process
:erlang.system_info(:process_count)
:erlang.system_info(:atom_count)