Reading — step 1 of 7
Learn
OCaml is fast — competitive with C in many benchmarks. But getting that performance requires understanding allocation, boxing, and the GC. Real World OCaml has a chapter on this.
OCaml's representation
- Integers — unboxed, tagged (61-bit on 64-bit systems; reserve 1 bit for tag, 1 for sign on some operations)
- Floats — boxed when stored in records, unless
[@@unboxed]or in a float-only record - Tuples — heap-allocated as a block of pointers
- Strings, arrays, records — heap-allocated
let x = 42 (* int — unboxed *)
let f = 3.14 (* float — usually boxed *)
let pair = (1, 2) (* tuple — heap allocated *)
Float boxing
Floats in records and arrays are usually BOXED unless you use special types:
type point = { x : float; y : float } (* x and y are BOXED individually *)
(* Better — float-only record gets unboxed: *)
type point = { mutable x : float; mutable y : float } (* unboxed inline *)
(* Float arrays are special: *)
let arr = Array.make 100 0.0 (* float array — unboxed if FLAT *)
Garbage collection
OCaml has a fast generational GC:
- Minor heap — small, fast collection for new allocations (~32MB default)
- Major heap — long-lived objects; collected by mark-sweep
Minor GC is fast (couple ms). Major GC is when most pauses come from. For latency-critical apps, tune Gc.set parameters or run major GC explicitly during quiet periods.
(* Inspect GC stats: *)
let stats = Gc.stat ()
Printf.printf "minor words: %.0f\n" stats.minor_words
Printf.printf "major words: %.0f\n" stats.major_words
Printf.printf "compactions: %d\n" stats.compactions
Avoiding allocations
(* Allocates a new tuple per call: *)
let swap (x, y) = (y, x)
(* No allocation — uses unboxed locals: *)
let swap_ints a b = (b, a) (* still allocates the tuple to return *)
For hot paths, prefer:
- Function arguments over tuples (no packaging)
- Records with strict field types
BytesandBufferfor strings- Arrays for fixed-size collections
Profiling tools
time(built-in shell) — wall-clockUnix.gettimeofday— finer in-program timingSpacetime— memory profiling (older versions)statmemprof— sampling memory profiler (newer)landmarks— annotation-based profiler (library)perf(Linux) — system-level profiling on the binary- OCaml 5+ Eventring — built-in tracing
(* Simple timing: *)
let time_it f x =
let start = Unix.gettimeofday () in
let result = f x in
let elapsed = Unix.gettimeofday () -. start in
Printf.eprintf "took %.3fs\n" elapsed;
result
Hot-loop tips
- Avoid allocating tuples in tight loops — use refs or extra args
- Use
Array.unsafe_get/Array.unsafe_setwhen you've already bounds-checked — skip checks in hot paths - Inlining — small functions are auto-inlined; mark with
[@@inline]to encourage - Pattern-match exhaustiveness — the compiler optimizes well-typed matches
- Closures — capture only what you need; large closures pin objects
Native compilation vs bytecode
ocamlfind ocamlopt ... # native, fast (default for production)
ocamlfind ocamlc ... # bytecode, smaller, portable
For performance, use ocamlopt (native compiler). Bytecode is mostly for debug builds or when native isn't available.
Multicore (OCaml 5+)
OCaml 5 (2022) added multicore support via Domains:
let d = Domain.spawn (fun () ->
(* runs on another core *)
heavy_computation ())
let result = Domain.join d
Domains share state but each has its own minor heap. For data parallelism, true parallel execution is now possible.
Common mistakes
- Premature optimization — profile first. Most code is fast enough.
- Float boxing in numeric records — use float-only records or arrays for hot paths.
- Tuple allocation in tight loops — pass extra arguments instead.
- Skipping
ocamlopt— bytecode is 5-10x slower. Use native for production. - Long-lived references holding short-lived data — promotes to major heap, slows GC. Drop references when done.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…