Reading — step 1 of 7
Learn
Erlang's runtime — the BEAM — has unusual performance characteristics. Understanding them shapes idiomatic Erlang code.
Process model
- Each Erlang process has its own ~300-byte initial heap
- Millions of processes per node are normal (WhatsApp ran 2M TCP connections per node)
- Process scheduling: preemptive, runtime-controlled (no co-op needed)
- One scheduler per CPU core; processes migrate as needed
Why processes are cheap
- No shared memory between processes — each has its own heap
- Garbage collection is per-process; no global pause
- Inter-process messages are COPIED (small) or REFERENCED (large binaries)
- Mailboxes are per-process FIFO queues
Per-process garbage collection
Each process is GC'd independently. Short-lived processes that finish quickly often die before GC kicks in — fastest possible memory reuse.
Long-running processes accumulate. The GC tunes itself based on heap growth; you can hint with process_flag(min_heap_size, N) for known-large workloads.
Binaries: refcounted vs heap
<<"small">> %% under 64 bytes — heap binary, copied with messages
<<"large data...">> %% over 64 bytes — refcounted binary, shared via reference
Large binaries (over 64 bytes) live OFF-HEAP and are refcounted. Sending one to another process sends a reference, not a copy. Big win for streaming binary protocols.
Leak hazard: if any process holds a reference to a big binary, the whole binary stays alive. Patterns like binary:copy/1 exist to extract a small piece without retaining the big original.
ETS for shared state
Processes don't share memory — but ETS tables provide concurrent in-memory key-value storage. Already covered; the key performance points:
- Reads are nearly free (concurrent, no locking)
- Writes serialize on a per-table lock by default;
{write_concurrency, true}reduces contention - Data is COPIED in/out — terms duplicate on every read
- Key lookups: O(log n) for ordered_set, O(1) average for set/bag
Performance gotchas
Strings as lists — "hello" is [104, 101, 108, 108, 111]. List operations on text are O(n) per character. For string manipulation, use binaries.
++ is O(n) on the LEFT operand:
L ++ X %% iterates L; if L is large and X is small, this is slow
Build lists from the right (using [H | T]) and reverse at the end:
build(List, Acc) -> [Item | Acc]. %% O(1) prepend
...
lists:reverse(Acc)
Map vs list of pairs — for many lookups, maps are O(log n) while proplists are O(n). Use maps.
Profiling
fprof— function-level profilingeprof— call counts and time per functioncprof— call counts only (low overhead)recon— production-friendly inspection (very useful)- observer — graphical introspection (
observer:start())
fprof:apply(my_module, my_function, [Args]).
fprof:profile().
fprof:analyse().
For production, prefer recon — designed for live systems.
Memory introspection
erlang:memory(). %% process heap, ets, atoms, binary, etc.
erlang:process_info(Pid). %% per-process details
erlang:system_info(process_count).
Common mistakes
- Spawning many short-lived processes for tiny work — usually fine, but create cost adds up. Pool when work is sub-millisecond per item.
- Atom explosion — list_to_atom on user input. Atoms aren't GC'd. Bounded atom table.
- Big binaries pinned by small references — common leak. Use binary:copy/1 to extract small slices without retaining the big source.
- Unbounded mailboxes — selective receive over a mailbox of millions of messages is O(n²). Apply backpressure.
- String concatenation in tight loops — use iolists (deeply nested lists of binaries/strings); flatten only at the I/O boundary.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…