Richard Eisenberg: How OCaml Locals Reduce Garbage-Collected Allocation

An annotated list-processing example shows why locality must be preserved both when values are created and when functions consume them.

RERichard EisenbergSpeaker · Jane Street
Channel
Jane Street
Published
Feb 20, 2024
Video
23:25
Read
5 min
Slides
5
Original
English
Open on YouTube
Scroll to read and the video shrinks to the bottom-right corner. Captions follow your reading language.
Reading languageAvailable in 2 languages
You're reading the original English.
SUMMARY

In 30 seconds

Richard Eisenberg uses an experimental Jane Street OCaml compiler to show how local values can reduce garbage-collected allocations. He builds and measures a list-processing loop, then rewrites its iterator and initializer to express locality. Compiler errors reveal the distinction between a locally constrained variable and locally allocated data, the roles of callbacks and closures, and how exclave allows a local result to be produced outside a function’s region.

  1. 1Measure allocationThe example uses OCAMLRUNPARAM=v=1024 to print allocation and collection statistics at program exit.
  2. 2A local-aware iteratorA list and its elements cannot escape, so the iterator and its callback need suitable locality guarantees.
  3. 3Creation mattersMarking a variable local does not turn a list already created by List.init into a local allocation.
  4. 4Return through an exclaveThe custom initializer uses exclave so the returned list is allocated outside the function’s own region.
  5. 5Interpret the resultAfter correcting the call and localizing the relevant values, the example reports 2,146 garbage-collected allocated words; local memory is still allocated and reclaimed by region.

Takeaways

  1. Local values cannot escape their region, allowing short-lived memory to be reclaimed when the region ends.
  2. A locally constrained variable can still hold a value originally allocated globally.
  3. Functions that consume local lists must also constrain any callback that receives local elements.
  4. Explicit locality annotations can reveal guarantees that the compiler otherwise infers.
  5. Exclave lets an expression allocate outside a function’s own region when returning a local result.
  6. A lower garbage-collection allocation counter does not mean the program performs no memory allocation.
FULL TEXT

Read the edited text by chapter

Transcript only
01

A list-allocation experiment

Eisenberg builds a repeatable loop that creates lists and counts multiples of five. Runtime statistics provide a baseline for garbage-collected allocation.

Richard Eisenberg demonstrates how OCaml locals can reduce garbage-collected allocation. Preventing a value from escaping a region is useful in itself, but Jane Street is also interested in the performance benefit of controlling where short-lived values are allocated.

The demonstration uses Jane Street’s experimental compiler branch. At the time of the video, the team intended to work with the community on bringing these features upstream. The example builds many small lists and counts the elements divisible by five.

The function runs a loop 10,000 times. Each iteration chooses a random length below ten, creates a list of random integers below 100 using List.init, and iterates through it, incrementing a reference when an element is divisible by five. A fixed random seed makes the result reproducible.

To see the allocation cost, Eisenberg consults the OCaml runtime documentation. Setting OCAMLRUNPARAM to v=1024 prints garbage-collection statistics at program exit, including allocated words. The initial bytecode run reports roughly 214,000 allocated words; reducing the number of iterations reduces the count.

02

Making the iterator local-aware

Loop bodies define regions, and local values must not escape. A custom iterator and its callback express the needed locality.

Each list is needed only within its loop iteration. In a language with manual memory management, one might explicitly free it there; locals aim to express the lifetime without manual freeing. Every function and every loop body defines a region, and a local value must not escape its region.

Baseline allocation measurement
SLIDE 1 / 5Baseline allocation measurementThe program creates a list each iteration and reports garbage-collection allocation statistics.

Marking the list local initially produces an escape error when it is passed to List.iter. In the compiler and library version demonstrated, the standard library function does not express the required locality. Eisenberg writes a recursive iter that handles an empty list or applies a function to the head and recurses on the tail.

The custom iterator compiles, but the allocation count remains high. He switches from ocamlc, the bytecode compiler used initially, to ocamlopt.opt, which produces native code for the machine. The count falls, but it is still much higher than the near-zero garbage-collected allocation he is seeking.

To understand the iterator’s behavior, Eisenberg writes an explicit type annotation. Without that annotation, the compiler can infer that the list does not escape. A type without a local annotation gives no such guarantee, so the compiler conservatively treats it as potentially escaping and rejects the call.

A local iterator and escape error
SLIDE 2 / 5A local iterator and escape errorThe compiler identifies the list passed to an iterator whose explicit type lacks the required locality.

Adding a local annotation to the list argument fixes that part of the type. A local list also means its elements cannot escape, so the callback must accept a local element as well; otherwise it might save the value elsewhere. With both annotations, the iterator type-checks.

03

Creating a local list

A local variable can contain a globally allocated list. Eisenberg writes an initializer and resolves locality and tail-position errors with explicit types and exclaves.

Even then the program still allocates. The local variable is assigned a list made by List.init, whose result is not guaranteed to have been allocated locally. A global value may be used locally: restricting a variable so it cannot escape does not retroactively change where the value was allocated.

Local iterator with List.init
SLIDE 3 / 5Local iterator with List.initThe list is constrained locally, but List.init still produces a globally allocated value.

To make the list local from the moment of creation, Eisenberg writes a replacement init. A recursive loop takes an index and an accumulator, applies a function to each index, and conses the result onto the accumulator. It starts at length minus one and finishes when the index falls below zero.

The replacement initially compiles and produces the expected result, 8,987, once it is actually called. Without locality annotations, however, it does not solve the allocation problem. He adds a type saying that the element-producing function returns a local value and init returns a local list.

The compiler then reports that a value escapes its region. The recursive loop needs a local accumulator, but return or tail position introduces another issue. A non-tail annotation temporarily gets further, though Eisenberg wants to preserve a tail call and returns to the issue.

He gives the loop an explicit type accepting and returning a local list. A syntax error illustrates that local_ is associated with the function arrow: placing parentheses in the wrong place prevents the annotation from reaching it. Once corrected, the compiler identifies another escape at the return.

Local initializer and return error
SLIDE 4 / 5Local initializer and return errorThe explicit loop type and compiler message show the need to handle the result’s region.

Every function defines a region, but the list being returned should be allocated outside the function’s own region. Eisenberg adds exclave, which ends the region early for that expression, and removes the temporary non-tail annotation. The outer init also needs an exclave for its local result.

With these changes, the program compiles. The local version reports roughly 90,000 allocated words, compared with about 177,000 when he switches back to List.init. The reduction is useful, but smaller than he expects, so he investigates the function arguments and closures.

04

Closures and the final measurement

Local function arguments and a local recursive loop remove more allocation. The demonstration distinguishes region allocation from the garbage collector’s counter.

Both iter and init accept functions. The function that generates a random value captures nothing from its environment, so the compiler can avoid allocating a closure for it. The iterator callback refers to the count reference, however, and captures state from its environment.

He marks the function arguments local as well. This exposes another escape issue: the recursive loop captures the local function, so the loop itself needs to be local. A local loop cannot then be used outside its region in an exclave; moving the exclave outward resolves that conflict.

At first the measurement still does not improve, because he has accidentally called List.init again. After changing the call to the custom init and recompiling, the program reports very little garbage-collected allocation: the screen shows 2,146 allocated words while the result remains 8,987.

Final allocation measurement
SLIDE 5 / 5Final allocation measurementAfter switching to the custom initializer, the output shows 2,146 garbage-collected allocated words.

The important distinction is that memory is still allocated for the local values. The counter is not saying the program uses no memory: it is showing far less allocation handled by automatic garbage collection. At the end of each loop region, that local memory can be reclaimed as a group, reducing work for the collector and potentially improving latency.

Eisenberg closes by pointing to the example code and says the next video will explain more about how this works and where the memory goes. The demonstration’s numbers apply to this small program and the experimental compiler used in the video.

More from this channel

1:48:29EN
JSJane Street· Oct 16, 2026How AI Is Changing Programming: John Krazit and Ron MinskyRon Minsky, John Krazit · Jane StreetJane Street’s AI tooling lead and the Signals and Threads host examine agent infrastructure, security, code review and the changing experience of writing software.Coding agentsDeveloper productivityEvals+2
9 slides1:05:45EN
JSJane Street· Sep 11, 2026Cyrus Omar on Live Programming and a Computational Commons for Humans and AICyrus OmarA demonstration of Hazel shows how immediate feedback, structured collaboration and carefully scoped permissions could support people and AI agents working together.Programming LanguagesHuman-AI CollaborationDeveloper workflows & testing+2
1:25:00EN
JSJane Street· Sep 2, 2026Eric Mannes on Turning Messy External Data Into Trading ResearchRon, Eric Mannes · Jane StreetFrom commodity futures and an Excel risk system to point-in-time datasets and AI, Eric Mannes explains the domain knowledge behind data engineering at Jane Street.Data quality & curationDeveloper workflows & testingInvesting+2
7 slides1:16:29EN
JSJane Street· Aug 26, 2026Grant Sanderson and Alok Puranik: Positional Encodings Through Group TheoryGrant Sanderson, Alok Puranik · 3Blue1BrownStarting with assumptions about attention, they derive matrix exponentials and connect rotations, decay and Jordan blocks to RoPE and ALiBi.Transformers & attentionMathematicsML fundamentals+1
1:34:29EN
JSJane Street· Jun 24, 2026Jacob Baskin on Building Superstore and Scheduling the HiveYaron Minsky, Jacob Baskin · Jane StreetThe Jane Street engineer traces the design choices behind a distributed analytical database and the challenge of allocating a growing research cluster.Infrastructure & deploymentSoftware engineeringDecision Making+1
1:34:36EN
JSJane Street· Jun 1, 2026Nate Foster: Treating the Network as a ProgramYaron Minsky, Nate Foster · Jane StreetFrom programmable switches to BGP, a programming-languages researcher explains how higher-level policies and formal models can make networks easier to change.Infrastructure & deploymentProgramming LanguagesSoftware engineering+2