Skip to content

Commit 03e10c4

Browse files
authored
feat(stdlib): Faster memory allocator (#2124)
1 parent 416936b commit 03e10c4

File tree

8 files changed

+341
-240
lines changed

8 files changed

+341
-240
lines changed

compiler/test/input/mallocTight.gr

Lines changed: 0 additions & 34 deletions
This file was deleted.

compiler/test/runner.re

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ let graindoc_out_file = name =>
3939
let gaindoc_in_file = name =>
4040
Filepath.to_string(Fp.At.(test_gaindoc_dir / (name ++ ".input.gr")));
4141

42-
let compile = (~num_pages=?, ~config_fn=?, ~hook=?, name, prog) => {
42+
let compile = (~num_pages=?, ~max_pages=?, ~config_fn=?, ~hook=?, name, prog) => {
4343
Config.preserve_all_configs(() => {
4444
Config.with_config(
4545
Config.empty,
@@ -49,11 +49,10 @@ let compile = (~num_pages=?, ~config_fn=?, ~hook=?, name, prog) => {
4949
| None => ()
5050
};
5151
switch (num_pages) {
52-
| Some(pages) =>
53-
Config.initial_memory_pages := pages;
54-
Config.maximum_memory_pages := Some(pages);
52+
| Some(pages) => Config.initial_memory_pages := pages
5553
| None => ()
5654
};
55+
Config.maximum_memory_pages := max_pages;
5756
Config.include_dirs :=
5857
[Filepath.to_string(test_libs_dir), ...Config.include_dirs^];
5958
let outfile = wasmfile(name);
@@ -63,7 +62,8 @@ let compile = (~num_pages=?, ~config_fn=?, ~hook=?, name, prog) => {
6362
});
6463
};
6564

66-
let compile_file = (~num_pages=?, ~config_fn=?, ~hook=?, filename, outfile) => {
65+
let compile_file =
66+
(~num_pages=?, ~max_pages=?, ~config_fn=?, ~hook=?, filename, outfile) => {
6767
Config.preserve_all_configs(() => {
6868
Config.with_config(
6969
Config.empty,
@@ -73,11 +73,10 @@ let compile_file = (~num_pages=?, ~config_fn=?, ~hook=?, filename, outfile) => {
7373
| None => ()
7474
};
7575
switch (num_pages) {
76-
| Some(pages) =>
77-
Config.initial_memory_pages := pages;
78-
Config.maximum_memory_pages := Some(pages);
76+
| Some(pages) => Config.initial_memory_pages := pages
7977
| None => ()
8078
};
79+
Config.maximum_memory_pages := max_pages;
8180
Config.include_dirs :=
8281
[Filepath.to_string(test_libs_dir), ...Config.include_dirs^];
8382
compile_file(~is_root_file=true, ~hook?, ~outfile, filename);
@@ -153,18 +152,7 @@ let open_process = args => {
153152
(code, out, err);
154153
};
155154

156-
let run = (~num_pages=?, ~extra_args=[||], file) => {
157-
let mem_flags =
158-
switch (num_pages) {
159-
| Some(x) => [|
160-
"--initial-memory-pages",
161-
string_of_int(x),
162-
"--maximum-memory-pages",
163-
string_of_int(x),
164-
|]
165-
| None => [||]
166-
};
167-
155+
let run = (~extra_args=[||], file) => {
168156
let stdlib = Option.get(Grain_utils.Config.stdlib_dir^);
169157

170158
let preopen =
@@ -186,7 +174,6 @@ let run = (~num_pages=?, ~extra_args=[||], file) => {
186174
let cmd =
187175
Array.concat([
188176
[|"grain", "run"|],
189-
mem_flags,
190177
[|"-S", stdlib, "-I", Filepath.to_string(test_libs_dir), preopen|],
191178
[|file|],
192179
extra_args,
@@ -306,6 +293,7 @@ let makeRunner =
306293
(
307294
test,
308295
~num_pages=?,
296+
~max_pages=?,
309297
~config_fn=?,
310298
~extra_args=?,
311299
~module_header=module_header,
@@ -315,8 +303,15 @@ let makeRunner =
315303
) => {
316304
test(name, ({expect}) => {
317305
Config.preserve_all_configs(() => {
318-
ignore @@ compile(~num_pages?, ~config_fn?, name, module_header ++ prog);
319-
let (result, _) = run(~num_pages?, ~extra_args?, wasmfile(name));
306+
ignore @@
307+
compile(
308+
~num_pages?,
309+
~max_pages?,
310+
~config_fn?,
311+
name,
312+
module_header ++ prog,
313+
);
314+
let (result, _) = run(~extra_args?, wasmfile(name));
320315
expect.string(result).toEqual(expected);
321316
})
322317
});
@@ -327,6 +322,7 @@ let makeErrorRunner =
327322
test,
328323
~check_exists=true,
329324
~num_pages=?,
325+
~max_pages=?,
330326
~config_fn=?,
331327
~module_header=module_header,
332328
name,
@@ -335,8 +331,15 @@ let makeErrorRunner =
335331
) => {
336332
test(name, ({expect}) => {
337333
Config.preserve_all_configs(() => {
338-
ignore @@ compile(~num_pages?, ~config_fn?, name, module_header ++ prog);
339-
let (result, _) = run(~num_pages?, wasmfile(name));
334+
ignore @@
335+
compile(
336+
~num_pages?,
337+
~max_pages?,
338+
~config_fn?,
339+
name,
340+
module_header ++ prog,
341+
);
342+
let (result, _) = run(wasmfile(name));
340343
if (check_exists) {
341344
expect.string(result).toMatch(expected);
342345
} else {
@@ -347,12 +350,13 @@ let makeErrorRunner =
347350
};
348351

349352
let makeFileRunner =
350-
(test, ~num_pages=?, ~config_fn=?, name, filename, expected) => {
353+
(test, ~num_pages=?, ~max_pages=?, ~config_fn=?, name, filename, expected) => {
351354
test(name, ({expect}) => {
352355
Config.preserve_all_configs(() => {
353356
let infile = grainfile(filename);
354357
let outfile = wasmfile(name);
355-
ignore @@ compile_file(~num_pages?, ~config_fn?, infile, outfile);
358+
ignore @@
359+
compile_file(~num_pages?, ~max_pages?, ~config_fn?, infile, outfile);
356360
let (result, _) = run(outfile);
357361
expect.string(result).toEqual(expected);
358362
})

compiler/test/suites/basic_functionality.re

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,6 @@ describe("basic functionality", ({test, testSkip}) => {
377377
~config_fn=smallestFileConfig,
378378
"smallest_grain_program",
379379
"",
380-
4769,
380+
5165,
381381
);
382382
});

compiler/test/suites/gc.re

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,25 @@ let makeGcProgram = (program, heap_size) => {
88
from "runtime/malloc" include Malloc
99
from "runtime/unsafe/memory" include Memory
1010
11-
@disableGC
12-
primitive heapStart = "@heap.start"
13-
14-
@disableGC
15-
let leak = () => {
16-
use WasmI32.{ (+), (-) }
17-
// find current memory pointer, subtract space for two malloc headers + 1 GC header
18-
let offset = Memory.malloc(8n) - 24n
19-
// Calculate how much memory is left
20-
let availableMemory = offset - (Malloc._RESERVED_RUNTIME_SPACE + heapStart())
21-
// Calculate how much memory to leak
22-
let toLeak = availableMemory - %dn
23-
// Memory is not reclaimed due to no gc context
24-
// This will actually leak 16 extra bytes because of the headers
25-
Memory.malloc(toLeak - 16n);
26-
void
11+
@unsafe
12+
let _ = {
13+
use WasmI32.{(*), (-), (==)}
14+
// Leak all available memory
15+
// The first call to malloc ensures it has been initialized
16+
Malloc.malloc(8n)
17+
Malloc.leakAll()
18+
// Next allocation will grow the memory by 1 page (64kib)
19+
// We'll manually leak all memory except what should be reserved for the test
20+
// Round reserved memory to nearest block size
21+
let reserved = %dn
22+
// If only one unit is requested, the allocator will include it in our next malloc,
23+
// so we request 2 instead
24+
let reserved = if (reserved == 1n) 2n else reserved
25+
// one page - 2 malloc headers - 1 gc header - extra morecore unit - reserved space
26+
let toLeak = 65536n - 16n - 8n - 64n - reserved * 64n
27+
Memory.malloc(toLeak)
2728
}
28-
leak();
29+
2930
%s
3031
|},
3132
heap_size,
@@ -43,6 +44,7 @@ describe("garbage collection", ({test, testSkip}) => {
4344
let assertRunGC = (name, heapSize, prog, expected) =>
4445
makeRunner(
4546
~num_pages=1,
47+
~max_pages=2,
4648
test_or_skip,
4749
name,
4850
makeGcProgram(prog, heapSize),
@@ -52,38 +54,42 @@ describe("garbage collection", ({test, testSkip}) => {
5254
makeErrorRunner(
5355
test_or_skip,
5456
~num_pages=1,
57+
~max_pages=2,
5558
name,
5659
makeGcProgram(prog, heapSize),
5760
expected,
5861
);
5962

6063
// oom tests
64+
// The allocator will use 2 units for the first allocation and then oom
6165
assertRunGCError(
6266
"oomgc1",
63-
48,
67+
2,
6468
"(1, (3, 4))",
6569
"Maximum memory size exceeded",
6670
);
67-
assertRunGC("oomgc2", 64, "(1, (3, 4))", "");
68-
assertRunGC("oomgc3", 32, "(3, 4)", "");
71+
// This requires only 2 units, but if only two are requested they would be
72+
// used by the first allocation
73+
assertRunGC("oomgc2", 3, "(1, (3, 4))", "");
74+
assertRunGC("oomgc3", 1, "(3, 4)", "");
6975

7076
// gc tests
7177
assertRunGC(
7278
"gc1",
73-
160,
79+
5,
7480
"let f = (() => (1, 2));\n {\n f();\n f();\n f();\n f()\n }",
7581
"",
7682
);
7783
/* https://github.com/grain-lang/grain/issues/774 */
7884
assertRunGC(
7985
"gc3",
80-
1024,
86+
17,
8187
"let foo = (s: String) => void\nlet printBool = (b: Bool) => foo(if (b) \"true\" else \"false\")\n\nlet b = true\nfor (let mut i=0; i<100000; i += 1) {\n printBool(true)\n}",
8288
"",
8389
);
8490
assertRunGCError(
8591
"fib_gc_err",
86-
256,
92+
5,
8793
{|
8894
let fib = x => {
8995
let rec fib_help = (n, acc) => {
@@ -102,7 +108,7 @@ describe("garbage collection", ({test, testSkip}) => {
102108
);
103109
assertRunGC(
104110
"fib_gc",
105-
512,
111+
9,
106112
{|
107113
let fib = x => {
108114
let rec fib_help = (n, acc) => {
@@ -121,7 +127,7 @@ describe("garbage collection", ({test, testSkip}) => {
121127
);
122128
assertRunGC(
123129
"loop_gc",
124-
256,
130+
5,
125131
{|
126132
for (let mut i = 0; i < 512; i += 1) {
127133
let string = "string"
@@ -134,7 +140,7 @@ describe("garbage collection", ({test, testSkip}) => {
134140
);
135141
assertRunGC(
136142
"long_lists",
137-
20000,
143+
350,
138144
{|
139145
from "list" include List
140146
use List.*
@@ -162,7 +168,6 @@ describe("garbage collection", ({test, testSkip}) => {
162168
|},
163169
"true\n",
164170
);
165-
assertFileRun("malloc_tight", "mallocTight", "");
166171
assertFileRun("memory_grow1", "memoryGrow", "1000000000000\n");
167172
assertMemoryLimitedFileRun(
168173
"loop_memory_reclaim",

docs/contributor/memory_management.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ We ultimately aim to replace Grain's bespoke memory management with the WebAssem
66

77
## Memory Allocator
88

9-
Grain uses a [memory allocator](https://github.com/grain-lang/grain/blob/main/stdlib/runtime/malloc.gr) derived from the `malloc`/`free` example given in Kernighan and Ritchie's ["The C Programming Language"](https://kremlin.cc/k&r.pdf) (K&R C), pages 185-188 (PDF page 199). This module exports the following values:
9+
More documentation about Grain's [memory allocator](https://github.com/grain-lang/grain/blob/main/stdlib/runtime/malloc.gr) can be found in that module. It exports the following values:
1010

1111
```grain
1212
/**
@@ -31,11 +31,9 @@ export let malloc: (nbytes: WasmI32) -> WasmI32
3131
export let free = (ap: WasmI32) => Void
3232
3333
/**
34-
* Returns the current free list pointer (used for debugging)
35-
*
36-
* @returns The free list pointer
34+
* Leaks all memory in all free lists; used for testing.
3735
*/
38-
export let getFreePtr = () => WasmI32
36+
export let leakAll = () => Void
3937
```
4038

4139
These functions should be familiar to programmers who have used `malloc` and `free` in C (and C-like languages). For further reading, refer to this Wikipedia page: [C dynamic memory allocation](https://en.wikipedia.org/wiki/C_dynamic_memory_allocation). The semantics of these functions align near-identically with those of C's corresponding functions.

0 commit comments

Comments
 (0)