Incremental: Self-Adjusting Computations in OCaml

Sebastian explains Jane Street’s dynamic dependency graph library and the algorithms behind efficient updates.

SFSebastian FunkVortrag · Jane Street
Kanal
Jane Street
Veröffentlicht
9. März 2017
Video
28:31
Lesezeit
7 Min.
Folien
8
Original
Englisch
Auf YouTube öffnen
Beim Scrollen schrumpft das Video in die rechte untere Ecke und läuft weiter. Untertitel folgen Ihrer Lesesprache.
LesespracheIn 2 Sprachen verfügbar
Diesen Artikel gibt es noch nicht auf Deutsch, daher wird das Original (Englisch) angezeigt.
ZUSAMMENFASSUNG

In 30 Sekunden

Jane Street developer Sebastian introduces Incremental, an OCaml library for self-adjusting computations, and demonstrates dynamic dependencies, stabilization and cutoffs. He traces the implementation from a simple two-pass algorithm through observer tracking, partial ordering and allocation improvements. He argues that building a widely used generic library makes it worthwhile to profile and optimize details that might be overlooked in a single application.

  1. 1A dynamic dependency graphThe library updates the computations and outputs affected by changing inputs, including changes to graph structure.
  2. 2Explicit stabilizationUsers choose when to process accumulated changes, enabling batching and consistent output.
  3. 3Bind and cutoffsBind selects dependencies dynamically, while cutoffs can stop insignificant changes from propagating.
  4. 4Correct order at a costTopological ordering fixes update ordering but a heap slowed the speaker’s workloads.
  5. 5Track observation explicitlyObservers and stop-observing prevent unnecessary work from persisting until delayed finalization.
  6. 6Replace the heapGrouping nodes by height restores performance while preserving the partial order needed for updates.

Das Wichtigste

  1. Incremental models changing inputs and computations as a dynamic dependency graph.
  2. Explicit stabilization batches changes and avoids exposing intermediate states.
  3. Bind changes dependencies dynamically, while cutoffs can suppress insignificant updates.
  4. Garbage collection, observation and recomputation order are central implementation concerns.
  5. A partial order based on node heights can replace a costly total-order heap for shallow graphs.
  6. GADTs helped reduce allocations in the implementation described in the talk.
VOLLTEXT

Den bearbeiteten Text nach Kapiteln lesen

Nur das Transkript
01

Self-adjusting computations

Sebastian introduces Incremental and its dynamic dependency graph. Trading, risk models and user interfaces motivate the library.

I am Sebastian, a developer at Jane Street. We use OCaml across trading systems and infrastructure, and I want to introduce Incremental, our library for self-adjusting computations. I will explain its interface and then some of the implementation challenges we encountered across several versions.

Incremental computation
FOLIE 1 / 8Incremental computationThe overview describes dynamic dependency graphs, minimal updates and application areas.

The idea, based on work by Acar on self-adjusting computation, is to represent a complex calculation and its changing inputs as a dependency graph. When inputs change, the library does the necessary work to update the outputs that users care about, rather than rerunning every calculation.

A spreadsheet is a useful analogy: input cells feed calculations, and changing inputs updates their dependents. The crucial difference in this comparison is that Incremental’s graph can itself change with the inputs. Its dependencies are dynamic rather than fixed in advance.

Our first application was trading systems. Market events feed calculations of quantities such as fair value, which in turn inform trading decisions. We have also used the library to update complex risk models in real time and to construct user interfaces that update only the affected portions when data changes.

We have also been exploring dynamic web applications using js_of_ocaml, which compiles OCaml to JavaScript. The same idea of describing an output from changing data can apply in the browser.

02

Building and stabilizing a graph

Map and map2 build dependencies, while explicit stabilization batches updates. A rectangle example demonstrates when results are recomputed.

The interface has a type for an incremental computation producing a value of some type, and a constant constructor for a value that never changes. Variables provide changing inputs. Map transforms an incremental value with a function, creating a new node dependent on the original.

Incremental interface
FOLIE 2 / 8Incremental interfaceThe interface shows map, map2, bind, stabilize and variable operations.

Map2 combines two incremental inputs using a function. That matters because map alone can fan out from a node, but cannot bring two dependencies together into a single computation. Bind, which I will return to, supplies the more powerful dynamic part of the interface.

Stabilize has a unit-to-unit type, but it performs important work. Changing an input does not immediately recalculate the graph; the user explicitly calls stabilize to perform the necessary updates. This is a deliberate design choice.

Explicit stabilization lets us batch updates and avoid exposing intermediate, inconsistent states. In a trading system, several changes arising from one market event may need to be considered together before presenting an indicator; recalculating after each partial change could lead to an undesirable decision.

For a simple example, define height and width as variables and use map2 to multiply them into a rectangle’s area. Register a callback, set the inputs and stabilize to print the result. A subsequent change prints nothing until the next stabilization, which applies the accumulated changes.

A rectangle area example
FOLIE 3 / 8A rectangle area exampleHeight and width feed a map2 computation that updates when stabilization runs.
03

Dynamic dependencies and cutoffs

Bind changes the graph as inputs change. Cutoffs prevent insignificant changes from propagating through expensive calculations.

Bind differs subtly from map in its type: its function returns another incremental computation. That lets the graph selected by the function change with its input. Map and map2 build static dependency structures; bind makes the dependency graph dynamic.

A conditional can be expressed with map3 over the condition and both branches, but then both branches remain dependencies. Changes to an inactive branch may still cause work. With bind, the selected branch becomes the dependency, so work underneath the unused branch need not propagate.

Cutoffs
FOLIE 4 / 8CutoffsA cutoff can stop small changes from propagating into a larger dependent computation.

Cutoffs provide another way to avoid unnecessary work. A node can prevent an update from propagating if its value has not changed enough, for example by a specified percentage. That can be useful when a small numerical change is not significant enough to justify recomputing a large statistical model.

04

The first stabilization algorithm

A two-pass implementation highlights difficulties with garbage collection and cutoffs. The speaker describes reference counting and optimistic restarts.

Implementing the library largely comes down to the stabilization algorithm. A first attempt uses two passes: starting at changed inputs, mark dependent nodes dirty and count children that must be recomputed; then recalculate from the leaves upward, waiting for each node’s dirty children.

A simple algorithm also has to cooperate with the garbage collector. If inputs remain live and point upward through the graph, old branches can remain reachable even when a bind changes the graph and users no longer need them. Without care, unused nodes accumulate.

Cooperating with garbage collection
FOLIE 5 / 8Cooperating with garbage collectionThe first implementation uses reference counts, pointers and sentinels to manage graph liveness.

One approach uses reference counts, pointers in both directions, and sentinels exposed to users as proxies for observed nodes. A finalizer on a collected sentinel can help clean up graph references when the corresponding output is no longer needed.

The two-pass algorithm has a separate problem with cutoffs. Its first pass marks parents dirty before it recomputes the cutoff node, so it cannot yet know whether a change should propagate. An optimistic workaround assumes it will not propagate and restarts if recomputation shows otherwise.

That workaround performed well in the original workload, where cutoffs often stopped propagation, but became painful under other usage patterns. A wrong optimistic assumption can force work to be repeated. We needed a better ordering for recomputation.

05

Ordering updates and managing observers

Topological ordering improves correctness but a heap costs performance. Explicit observers address obsolete computations retained while finalizers are delayed.

A topological order and a heap can ensure that children are updated before their parents. Dynamic binds complicate the ordering: recomputing a full topological sort whenever the graph changes would be expensive. Pseudo-timestamps allow a newly created node to be placed between existing nodes.

The labels need room for future insertions. A data structure can distribute them using heuristics and occasionally pay for an expensive redistribution when it runs out of space. This approach was relatively easy to implement, but in our measurements the heap made it roughly 1.5 to 4 times slower than the first version.

Heap performance cost
FOLIE 6 / 8Heap performance costThe topological-order implementation was measured at 1.5 to 4 times slower than version one.

Another problem was garbage retained until finalizers ran. In the runtime behavior we observed, finalization could be delayed a long time. Until the collector recognizes that nodes are dead, input changes can keep triggering work in obsolete parts of the graph, with especially bad behavior when many binds are involved.

The interface needs an explicit notion of what is observed and when it stops being observed. An observer tracks outputs that users care about, and a stop-observing operation says that an output no longer needs computation. This avoids relying solely on delayed collection to stop obsolete work.

06

Partial ordering and performance

Node heights replace the heap and pseudo-heights avoid unnecessary recalculation. GADTs reduce allocation and improve speed further.

The heap still costs too much. In version six we took advantage of the fact that a total order is unnecessary: nodes at the same height cannot depend on one another, so they can be processed in either order. Many of our graphs have millions of nodes but heights of only around 20 or 30.

Instead of a heap, a small array of lists can group nodes by height. Dynamic graphs still create a problem: recalculating all heights on every bind would waste work. We use pseudo-heights, increasing a height when necessary but not decreasing it simply because a dependency moved.

A pathological graph that repeatedly alternates can make pseudo-heights grow indefinitely. Our practical safeguard is to fail when a configurable height limit is exceeded—128 in the example. In our workloads, this has not been a practical problem.

After six versions, we recovered performance comparable to the original baseline while making cutoffs work and binds efficient. It was frustrating to spend so much effort making the initial implementation slower before eventually getting back to its speed with correct behavior.

Results after eliminating the heap
FOLIE 7 / 8Results after eliminating the heapVersion six restored baseline performance while making cutoffs and binds work efficiently.

Further work yielded another two- to threefold improvement. A graph contains heterogeneous incrementals—integers, floats, strings and other types. One common way to hide those differences is to store closures that operate on nodes, but closures allocate memory and can be expensive.

Reducing allocations with GADTs
FOLIE 8 / 8Reducing allocations with GADTsThe later optimization reduced allocations and improved performance in real applications.

Generalized algebraic data types, or GADTs, let the implementation represent and pattern-match over heterogeneous node kinds without relying on the same closure-heavy approach. The result greatly reduced allocation and sped up real applications. It is an example of a type-system feature proving useful for practical performance.

07

Optimizing a shared library

Sebastian explains why a general-purpose library makes detailed profiling worthwhile, using record layout as an example. He closes with possible future improvements and points to the open-source implementation.

Applications using Incremental are often limited by time spent in the library itself. That is a reason to take on the sometimes painful work of building a reusable abstraction: a generic library can be profiled across many uses, making optimizations worthwhile that a one-off application might never receive.

For example, a node’s record contains the information needed by several use cases. We tried rearranging its fields to find an ordering that worked better for caches, and obtained an improvement. It is a simple optimization, but one that a developer writing only a single application might not consider.

There is still room for further work, such as explicit memory management or collapsing nodes, though neither is as trivial as it may sound. Incremental is open source on GitHub, and its documented interface is available for anyone who wants to explore it.

Mehr aus diesem Kanal

6 Folien15:33EN
JSJane Street· 26. Okt. 2026Figgie Strategy: Reading Hands, Winning Majorities and Pricing CardsBradley, Mike · Jane StreetJane Street traders Bradley and Mike analyze three games to show how private information, market competition and timing affect card prices.Decision MakingMathematicsFintech
1:48:29EN
JSJane Street· 16. Okt. 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-AgentenDeveloper productivityEvals+2
1:36:37EN
JSJane Street· 23. Sept. 2026Aaron Bauer on Teaching Software Engineering in the Age of AIRon Minsky, Aaron Bauer · Jane StreetHow learning goals, active practice and editor design shape engineering education when models can generate the code.AI in EducationSoftware engineeringEducation+2
9 Folien1:05:45EN
JSJane Street· 11. Sept. 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 CollaborationEntwicklungs-Workflows & Tests+2
1:25:00EN
JSJane Street· 2. Sept. 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 & curationEntwicklungs-Workflows & TestsInvesting+2
7 Folien1:16:29EN
JSJane Street· 26. Aug. 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-Grundlagen+1