[@zero_alloc] and the compiler refuses to build if anything in its call tree touches the heap." name=description><br>The feature in OxCaml that more languages should steal - The Consensus
The Consensus Weekly<br>Get new deep dives, plus jobs and funding in software infrastructure, free in your inbox every week.Subscribeocaml<br>The feature in OxCaml that more languages should steal<br>In most languages, you hunt down allocations with a profiler and they creep back the moment you touch the hot path.
Jane Street's superset of OCaml lets you flip that around: annotate a function with [@zero_alloc] and the compiler refuses to build if anything in its call tree touches the heap.By Phil Eaton·June 27, 2026 Focus<br>You are getting early access to this article as a subscriber. Your support makes articles like this possible. Thank you.OxCaml, Jane Street’s superset of OCaml, allows you to assert that entire functions do not allocate (on the heap). And if you start allocating within that call tree, the compiler will fail and tell you that you allocated. While you might be able to achieve this with static analysis, there are few mainstream languages that let you do this in the compiler itself. Swift and Clang are the only other exceptions I know of.1In most languages (Java, Go, C#, Rust, Zig, OCaml, etc.) the process is reversed: you take a profiler to try and find allocations (usually in loops that happen millions of times). Then you go and eliminate or minimize the allocations. But as soon as you edit a line of code in the hot path, you might forget the context and start allocating again, and you’re back to square one with a profiler.In D you can mark a function as @nogc but it doesn't stop you from heap allocating without the garbage collector. In Zig (and maybe more recent Rust) you might be able to minimize regression through convention by not passing an allocator to a function. But convention can be ignored and bypassed. Why not let the compiler do the work?In this article we'll take a look at how OxCaml's [@zero_alloc] assertion works.Prerequisites#<br>First install OxCaml (this will take a while).sudo apt-get -y install unzip bubblewrap hyperfine build-essential autoconf<br>bash -c "sh<br>opam init<br>opam switch create 5.2.0+ox --repos ox=git+https://github.com/oxcaml/opam-repository.git,default<br>eval $(opam env --switch 5.2.0+ox)
Now let's try it out.A naive program to sum CSV data#<br>Let’s write a naive OCaml program (no OxCaml features yet) to sum up rows of a CSV. It might not be idiomatic but the point is to show off this zero allocation assertion feature.First we open the file at the path in the first command line argument. Then we call sum_column with the open file and pass along the second command line argument to indicate the column to sum.let () =<br>let path = Sys.argv.(1) in<br>let col = int_of_string Sys.argv.(2) in<br>let ic = open_in_bin path in<br>let sum = sum_column ic col in<br>close_in ic;<br>Printf.printf "sum=%d\n" sum
main_alloc.mlRemember that parentheses for function calls in OCaml are not necessary. The .(X) syntax is array access. And open_in_bin and close_in are standard library functions for files.Next we’ll iterate through the file in 64KB chunks.let chunk = 1 lsl 16
let sum_column (ic: in_channel) (col: int) =<br>let buf = Bytes.create chunk in<br>let st: state = { column = 0; field = Buffer.create 16; sum = 0 } in<br>let continue = ref true in<br>while !continue do<br>let got = input ic buf 0 chunk in<br>if got > 0 then sum_column_incremental st buf got col<br>else continue := false<br>done;<br>st.sum
main_alloc.mlRemember that ref is a wrapper for mutable values in OCaml. ! is the dereference operator for ref types and := is assignment for ref types.Now within sum_column_incremental we’ll iterate through the buffer, character at a time. We naively accumulate the field characters into a buffer that we can convert into a string once we’ve parsed past the column the user asked us to sum.type state = { mutable column : int; field : Buffer.t; mutable sum : int }
let sum_column_incremental (st: state) (buf: Bytes.t) (len: int) (col: int): unit =<br>for i = 0 to len - 1 do<br>let c = Bytes.get buf i in<br>if (c = ',' || c = '\n') && st.column = col then begin<br>let value = int_of_string (Buffer.contents st.field) in<br>st.sum st.sum + value;<br>Buffer.clear st.field;<br>end;<br>match c with<br>| '\n' -><br>st.column 0<br>| ',' -><br>st.column st.column + 1<br>| c -><br>if st.column = col then<br>Buffer.add_char st.field c<br>done
main_alloc.mlRemember that is record field assignment in OCaml.This is not a great CSV parser. We don’t try to handle quotes or anything.Let’s create a large CSV dataset (2 columns, 100 million rows) to run it against.awk -v n=100000000 'BEGIN { srand(1); for (i = 1; i > data.csv
And try it out, summing up the second column.$ ocamlopt main_alloc.ml -o main_alloc<br>$ ./main_alloc data.csv 1<br>sum=49947827758
Not bad! But we’re unhappy with the performance. Hyperfine tells us it usually takes around 13 seconds to complete on this Digital Ocean virtual machine with 4vCPUs...