Skip to content

Commit 04fb351

Browse files
authored
Update "Local allocation" documentation (#3479)
1 parent c15536b commit 04fb351

File tree

7 files changed

+453
-391
lines changed

7 files changed

+453
-391
lines changed

jane/doc/extensions/local/intro.md

-161
This file was deleted.

jane/doc/extensions/modes/reference.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ OCaml.
55

66
The mode system in the compiler tracks various properties of values, so that certain
77
performance-enhancing operations can be performed safely. For example:
8-
- Locality tracks escaping. See [the local allocations reference](../local/reference.md)
8+
- Locality tracks escaping. See [the local allocations reference](../stack/reference.md)
99
- Uniqueness and linearity tracks aliasing. See [the uniqueness reference](../uniqueness/reference.md)
1010
- Portability and contention tracks inter-thread sharing.
1111
<!-- CR zqian: reference for portability and contention -->

jane/doc/extensions/stack/intro.md

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Introduction to Stack Allocations
2+
3+
See also the full feature [reference](reference.md) and [common pitfalls](pitfalls.md).
4+
5+
This page describes how OCaml sometimes allocates values on a stack,
6+
as opposed to its usual behavior of allocating on the heap.
7+
This helps performance in a couple of ways: first, the same few hot
8+
cache lines are constantly reused, so the cache footprint is lower than
9+
usual. More importantly, stack allocations will never trigger a GC,
10+
and so they're safe to use in low-latency code that must currently be
11+
zero-alloc.
12+
13+
Because of these advantages, values are allocated on a stack whenever
14+
possible. Of course, not all values can be allocated on a stack: a value that is
15+
used beyond the scope of its introduction must be on the heap. Accordingly,
16+
the compiler uses the _locality_ of a value to determine where it will be
17+
allocated: _local_ values go on the stack, while _global_ ones must go on the
18+
heap.
19+
20+
Though type inference will infer where stack allocation is possible, it is
21+
often wise to annotate places where you wish to enforce locality and stack
22+
allocation. This can be done by labeling an allocation as a `stack_` allocation:
23+
24+
```ocaml
25+
let x2 = stack_ { foo; bar } in
26+
...
27+
```
28+
29+
However, for this to be safe, stack-allocated values must not be used after
30+
their stack frame is freed. This is ensured by the type-checker as follows.
31+
A stack frames is represented as a _region_ at compile time, and each
32+
stack-allocated value lives in the surrounding region (usually a function body).
33+
Stack-allocated values are not allowed to escape their region. If they do,
34+
you'll see error messages:
35+
36+
```ocaml
37+
let foo () =
38+
let thing = stack_ { foo; bar } in
39+
thing
40+
^^^^^
41+
Error: This value escapes its region
42+
```
43+
44+
Most allocations in OCaml can be stack-allocated: tuples, records, variants,
45+
closures, boxed numbers, etc. Stack allocations are also possible from C stubs,
46+
although this requires code changes to use the new `caml_alloc_local` instead of
47+
`caml_alloc`. A few types of allocation cannot be stack-allocated, though,
48+
including first-class modules, classes and objects, and exceptions. The contents
49+
of mutable fields (inside `ref`s, `array`s and mutable record fields) also
50+
cannot be stack-allocated.
51+
52+
The `stack_` keyword works shallowly: it only forces the immediately following allocation
53+
to be on stack. Putting it before an expression that is not an allocation (such as a
54+
complete function application) leads to a type error.
55+
56+
## Local parameters
57+
58+
Generally, OCaml functions can do whatever they like with their
59+
arguments: use them, return them, capture them in closures or store
60+
them in globals, etc. This is a problem when trying to pass around
61+
stack-allocated values, since we need to guarantee they do not
62+
escape.
63+
64+
The remedy is that we allow the `local_` keyword to appear on
65+
function parameters:
66+
67+
```ocaml
68+
let f (local_ x) = ...
69+
```
70+
71+
A local parameter is a promise by a function not to let a particular
72+
argument escape its region. In the body of f, you'll get a type error
73+
if x escapes, but when calling f you can freely pass stack-allocated values as
74+
the argument. This promise is visible in the type of f:
75+
76+
```ocaml
77+
val f : local_ 'a -> ...
78+
```
79+
80+
The function f may be equally be called with stack- or
81+
heap-allocated values: the `local_` annotation places obligations only on the
82+
definition of f, not its uses.
83+
84+
<!-- CR zqian: factor the generic mode stuff into a dedicated document. -->
85+
86+
Even if you're not interested in performance benefits, local
87+
parameters are a useful new tool for structuring APIs. For instance,
88+
consider a function that accepts a callback, to which it passes some
89+
mutable value:
90+
91+
```ocaml
92+
let uses_callback ~f =
93+
let tbl = Foo.Table.create () in
94+
fill_table tbl;
95+
let result = f tbl in
96+
add_table_to_global_registry tbl;
97+
result
98+
```
99+
100+
Part of the contract of `uses_callback` is that it expects `f` not to
101+
capture its argument: unexpected results could ensue if `f` stored a
102+
reference to this table somewhere, and it was later used and modified
103+
after it was added to the global registry. Using `local_`
104+
annotations allows this constraint to be made explicit and checked at
105+
compile time, by giving `uses_callback` the signature:
106+
107+
```ocaml
108+
val uses_callback : f:(local_ int Foo.Table.t -> 'a) -> 'a
109+
```
110+
111+
## Inference
112+
113+
The examples above use the `stack_` keyword to mark stack
114+
allocations. In fact, this is not necessary, and the compiler will use
115+
stack allocations by default where possible.
116+
117+
The only effect of the keyword on an allocation is to change the
118+
behavior for escaping values: if the allocated value looks like it escapes
119+
and therefore cannot be stack-allocated, then without the keyword
120+
the compiler will allocate this value on the GC heap as usual, while
121+
with the keyword it will instead report an error.
122+
123+
Inference can even determine whether parameters are local, which is
124+
useful for helper functions. It's less useful for top-level functions,
125+
though, as whether their parameters are local is generally forced by
126+
their signature in the mli file, where no inference is performed.
127+
128+
Inference does not work across files: if you want e.g. to pass a local
129+
argument to a function in another module, you'll need to explicitly
130+
mark the local parameter in the other module's mli.
131+
132+
## More control
133+
134+
There are a number of other features that allow more precise control
135+
over which values are stack-allocated, including:
136+
137+
- **Stack-allocated closures**
138+
139+
```ocaml
140+
let f = stack_ (fun a b c -> ...)
141+
```
142+
143+
defines a function `f` whose closure is itself stack-allocated.
144+
145+
- **Local-returning functions**
146+
147+
```ocaml
148+
let f a = exclave_
149+
...
150+
```
151+
152+
defines a function `f` which puts its stack-allocated values in its
153+
caller's region.
154+
155+
- **Global fields**
156+
157+
```ocaml
158+
type 'a t = { global_ g : 'a }
159+
```
160+
161+
defines a record type `t` whose `g` field is always known to be
162+
heap-allocated (and may freely escape regions), even though the record
163+
itself may be stack-allocated.
164+
165+
For more details, read [the reference](./reference.md).

jane/doc/extensions/local/pitfalls.md renamed to jane/doc/extensions/stack/pitfalls.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
# Some Pitfalls of Local Allocations
1+
# Some Pitfalls of Stack Allocations
22

33
This document outlines some common pitfalls that may come up when
4-
trying out local allocations in a new code base, as well as some
4+
trying out stack allocations in a new code base, as well as some
55
suggested workarounds. Over time, this list may grow (as experience
66
discovers new things that go wrong) or shrink (as we deploy new
77
compiler versions that ameliorate some issues).
88

9-
If you want an introduction to local allocations, see the [introduction](intro.md).
9+
If you want an introduction to stack allocations, see the [introduction](intro.md).
1010

1111
## Tail calls
1212

1313
Many OCaml functions just happen to end in a tail call, even those
1414
that are not intentionally tail-recursive. To preserve the
1515
constant-space property of tail calls, the compiler applies special
16-
rules around local allocations in tail calls (see [the
16+
rules around locality in tail calls (see [the
1717
reference](./reference.md)).
1818

1919
If this causes a problem for calls that just happen to be in tail
@@ -41,7 +41,7 @@ after `func` returns.
4141

4242
## Partial applications with local parameters
4343

44-
To enable the use of local allocations with higher-order functions, a
44+
To enable the use of stack allocations with higher-order functions, a
4545
necessary step is to add local annotations to function types,
4646
particularly those of higher-order functions. For instance, an
4747
unlabeled `iter` function may become:
@@ -50,12 +50,12 @@ unlabeled `iter` function may become:
5050
val iter : local_ ('a -> unit) -> 'a t -> unit
5151
```
5252

53-
thus allowing locally-allocated closures to be used as the first
53+
thus allowing stack-allocated closures to be used as the first
5454
parameter.
5555

5656
However, this is unfortunately not an entirely backwards-compatible
5757
change. The problem is that partial applications of `iter` functions
58-
with the new type are themselves locally allocated, because they close
58+
with the new type are themselves `local`, because they close
5959
over the possibly-local `f`. This means in particular that partial
6060
applications will no longer be accepted as module-level definitions:
6161

@@ -84,9 +84,9 @@ parameter of functions.
8484

8585
## Typing of (@@) and (|>)
8686

87-
The typechecking of (@@) and (|>) changed slightly with the local
88-
allocations typechecker, in order to allow them to work with both
89-
local and nonlocal arguments. The major difference is that:
87+
The type-checking of (@@) and (|>) changed slightly with locality,
88+
in order to allow them to work with both
89+
local and global arguments. The major difference is that:
9090

9191
f x @@ y
9292
y |> f x

0 commit comments

Comments
 (0)