Skip to content

Instantly share code, notes, and snippets.

@damiendoligez
Created July 9, 2026 10:24
Show Gist options
  • Select an option

  • Save damiendoligez/6873546560f5b45d4fe171bd48b1a935 to your computer and use it in GitHub Desktop.

Select an option

Save damiendoligez/6873546560f5b45d4fe171bd48b1a935 to your computer and use it in GitHub Desktop.
OCaml: simple benchmark for measuring GC response to o parameter
(* Simple GC benchmark.
after first iteration, live memory is constant at 100M words (800M bytes)
We are testing the handling of off-heap memory and ephemerons by the GC
in various proportions.
We have 3 parameters:
e1 : ratio of off-heap to on-heap allocations
e2 : ratio of ephemerons to on-heap allocations
s : size of allocations
Note that the parameters are constrained by the data structure:
- We use a 3-tuple (4 words) for each allocation.
- The bigarray has a 7-word block on the heap.
- The weak array has 3 words of overhead.
- The normal array has 1 word of overhead and at least one pointer.
The total is 16 words for each allocation, so
e1 <= (s-16) / 16
e2 <= (s-16) / s
*)
external max_rss : unit -> int64 = "rusage_maxrss"
let clip x (min, max) =
if x < min then min
else if x > max then max
else x
(* Size of each allocation set *)
let s = 1000
(* off-heap ratio: 0.0 to 4.0 Note that 4.0 is unreasonably large. *)
let e1 = 0.0
(* ephemeron ratio: 0.0 to 0.7 Note that 0.7 is unreasonably large. *)
let e2 = 0.05
(* Number of elements *)
(* Note that s*n must be 100M, modulo rounding errors. *)
let n = 100_000_000 / s
;; assert (n <= 1_000_000)
;; Printf.printf "s: %d\nn: %d\n" s n
(* data structure overhead *)
let s_ovh = 16
(* s_off = size of off-heap allocation *)
(* We have e1 = s_off / (s-s_off) so s_off = s * e1/(e1+1) *)
let s_off = int_of_float (float s *. e1 /. (e1 +. 1.))
let s_off = clip s_off (0, s - s_ovh)
(* s_on = size of on-heap allocations
s_on + s_off = s
*)
let s_on = s - s_off
;; assert (s_on >= s_ovh)
(* s_ephe = size of ephemeron allocations *)
(* we have e2 = s_ephe / s_on so s_ephe = e2 * s_on *)
let s_ephe = int_of_float (e2 *. float s_on)
let s_ephe = clip s_ephe (0, s_on - s_ovh)
let s_ephe = if s_ephe < 4 then 0 else s_ephe
(* s_norm = size of normal array allocations, not counting the first field *)
let s_norm = s_on - s_ovh - s_ephe
;; assert (s_norm >= 0)
;; assert (s_on = s_norm + s_ovh + s_ephe)
;; Printf.printf "s_on: %d\ns_norm: %d\ns_off: %d\ns_ephe: %d\ns_ovh: %d\n"
s_on s_norm s_off s_ephe s_ovh
let actual_e1 = (float s_off /. float s_on)
let actual_e2 = (float s_ephe /. float s_on)
;; Printf.printf "e1: %.2f\n" actual_e1
;; Printf.printf "e2: %.2f\n" actual_e2
;; Printf.printf "title: s:%d off:%.2f ephe:%.2f V=%s\n"
s actual_e1 actual_e2 Sys.ocaml_version
(* Size of on-heap data: live stuff plus the 1M array *)
let on_sz = 1 + n * s_on / 1_000_000
(* Size of off-heap data. *)
let off_sz = n * s_off / 1_000_000
;; Printf.printf "inf: (o/100+1) * %d + %d\n" on_sz off_sz
;; Printf.printf "sup: (o/100+1.1) * %d + %d\n" on_sz off_sz
;; flush stdout
let mkweak n = (Weak.create n : int array Weak.t)
let constant_bigarray = Bigarray.(Array1.create float64 c_layout 1)
let constant_weak = mkweak 1
let make_elem j =
let ba =
if s_off = 0
then constant_bigarray
else Bigarray.(Array1.create float64 c_layout s_off)
in
for i = 0 to s_off / 100 - 1 do
Bigarray.Array1.set ba (i*100) 1.0
done;
let w = if s_ephe = 0 then constant_weak else mkweak (s_ephe - 3) in
let ar =
let sz =
s_norm + (if s_off = 0 then 7 else 0) + (if s_ephe = 0 then 3 else 0)
in
assert (sz >= 0);
Array.make (sz+1) j
in
(ba, w, ar)
(* We keep this array at 1M to make everything comparable
with different values of s (and n).
*)
let a = Array.make 1_000_000 (make_elem 0)
let main () =
(* Preheat minor heap. *)
while (Gc.quick_stat ()).minor_collections < 2 do
ignore (Sys.opaque_identity (ref 42))
done;
(* Allocate and preheat remembered set. *)
while (Gc.quick_stat ()).minor_collections < 4 do
a.(0) <- make_elem 0
done;
(* Measure the overhead: process size before we start allocating. *)
let rss_before = max_rss () in
let stat_startup = Gc.quick_stat () in
while (Gc.quick_stat ()).major_collections
< stat_startup.major_collections + 50
do
for j = 0 to n-1 do
a.(j) <- make_elem j;
done
done;
let rss_after = max_rss () in
let rss_alloc =
match Sys.getenv_opt "RAWRSS" with
| None -> Int64.(to_float (sub rss_after rss_before))
| Some _ -> Int64.to_float rss_after
in
Printf.printf "%.0f maximum resident set size\n" rss_alloc;
Printf.printf "%.0f RSS overhead\n" (Int64.to_float rss_before);
flush stdout;
;; main ()
@edwintorok

Copy link
Copy Markdown
--- prog1.ml    2026-07-09 18:27:31.416380946 +0100
+++ prog.ml     2026-07-09 18:43:37.947431826 +0100
@@ -40,8 +40,37 @@
 ;; assert (n <= 1_000_000)
 ;; Printf.printf "s: %d\nn: %d\n" s n
 
+let mkweak n = (Weak.create n : int array Weak.t)
+
+let measure_live_words_delta name n f =
+  Gc.full_major ();
+  let q0 = Gc.quick_stat () in
+  let r = f n in
+  Gc.full_major ();
+  let q1 = Gc.quick_stat () in
+  let _alive = Sys.opaque_identity r in
+  let live_words = q1.Gc.live_words - q0.Gc.live_words - Obj.reachable_words (Obj.repr q0) - n in
+  Printf.printf "%s live words overhead = %d\n%!" name live_words;
+  live_words
+let mk_ba_one _ = Bigarray.(Array1.create float64 c_layout 1)
+let mk_weak_one _ = mkweak 1
+let mk_array n = Array.make n 0
+let mk_tuple x = x,x,x
+let ovh_bigarray = measure_live_words_delta "bigarray" 0 mk_ba_one
+let ovh_weak = measure_live_words_delta "weak" 0 mk_weak_one
+let ovh_weak_125 = measure_live_words_delta "weak 125" 125 mkweak
+let ovh_weak_126 = measure_live_words_delta "weak 126" 126 mkweak
+let ovh_weak_large = measure_live_words_delta "weak large" 512 mkweak
+let ovh_array = measure_live_words_delta "small array" 1 mk_array
+let ovh_large_array = measure_live_words_delta "large array" 512 mk_array
+let ovh_tuple = measure_live_words_delta "tuple" 0 mk_tuple
+
 (* data structure overhead *)
-let s_ovh = 16
+let s_ovh' = ovh_bigarray + ovh_weak + ovh_large_array + ovh_tuple
+let s_ovh = 18
+
+let () = if s_ovh <> s_ovh' then
+  Printf.eprintf "s_ovh: %d != %d\n" s_ovh s_ovh';;
 
 (* s_off = size of off-heap allocation *)
 (* We have e1 = s_off / (s-s_off) so s_off = s * e1/(e1+1) *)
@@ -60,6 +89,8 @@
 let s_ephe = clip s_ephe (0, s_on - s_ovh)
 let s_ephe = if s_ephe < 4 then 0 else s_ephe
 
+let s_ovh = if s_ephe >= 126 then s_ovh + 2 else s_ovh
+
 (* s_norm = size of normal array allocations, not counting the first field *)
 let s_norm = s_on - s_ovh - s_ephe
 ;; assert (s_norm >= 0)
@@ -82,7 +113,6 @@
 ;; flush stdout
 
 
-let mkweak n = (Weak.create n : int array Weak.t)
 
 let constant_bigarray = Bigarray.(Array1.create float64 c_layout 1)
 let constant_weak = mkweak 1
@@ -96,7 +126,7 @@
   for i = 0 to s_off / 100 - 1 do
     Bigarray.Array1.set ba (i*100) 1.0
   done;
-  let w = if s_ephe = 0 then constant_weak else mkweak (s_ephe - 3) in
+  let w = if s_ephe = 0 then constant_weak else mkweak s_ephe in
   let ar =
     let sz =
       s_norm + (if s_off = 0 then 7 else 0) + (if s_ephe = 0 then 3 else 0)
@@ -106,6 +136,24 @@
   in
   (ba, w, ar)
 
+let ovh_one =
+  measure_live_words_delta "make_elem" (s_on) make_elem
+let () =
+  if ovh_one <> 0 then
+    Printf.eprintf "ovh_test: expected to be, but have %d overhead\n%!" ovh_one
+
+(*
+let dummy = make_elem 0
+let a0 = Array.make 1000 dummy
+let seti i _ = a0.(i) <- make_elem i
+let mk n = Array.iteri seti a0; a0
+
+let ovh_test = measure_live_words_delta "make_elem" (Array.length a0 * s_on) mk
+let () =
+  if ovh_test <> 0 then
+    Printf.eprintf "ovh_test: expected to be, but have %d overhead\n%!" ovh_test
+*)
+
 (* We keep this array at 1M to make everything comparable
    with different values of s (and n).
  *)

This tweaks some of the constant overheads. On OCaml 5.5 s_ovh should be 18 instead of 16 (due to the extra overhead from LARGE_ALLOC_HEADER_SZ).
Also the s_ephe - 3 seemed wrong (and resulted in a measured -3 words difference between desired s_on and actual), removing it makes make_elem return exactly s_on.
Also when weak arrays are >= 126 words they need 2 extra words (this matches with the transition from 1 to 3 words of overhead for regular arrays, 126 + 2 extra words in caml_weak_create = 128).

I haven't checked other OCaml versions.

@edwintorok

edwintorok commented Jul 10, 2026

Copy link
Copy Markdown

I noticed that the RSS is not stable even when the program is unchanged (likely due to the C allocator).
To confirm that the random noise indeed comes from outside of the OCaml GC I called malloc_stats on Linux:

#ifdef __GLIBC__
#include "malloc.h"
#else
static void malloc_stats(void) {}
#endif

value stub_malloc_stats(value u)
{
    malloc_stats();
}
--- prog.ml.1   2026-07-10 13:59:05.271295118 +0100
+++ prog1.ml    2026-07-10 13:57:10.004647611 +0100
@@ -20,6 +20,7 @@
 *)
 
 external max_rss : unit -> int64 = "rusage_maxrss"
+external malloc_stats: unit -> unit = "stub_malloc_stats"
 
 let clip x (min, max) =
   if x < min then min
@@ -29,7 +30,7 @@
 (* Size of each allocation set *)
 let s = 1000
 (* off-heap ratio: 0.0 to 4.0  Note that 4.0 is unreasonably large. *)
-let e1 = 0.0
+let e1 = 0.4
 (* ephemeron ratio: 0.0 to 0.7  Note that 0.7 is unreasonably large. *)
 let e2 = 0.05
 
@@ -135,6 +136,7 @@
   done;
 
   let rss_after = max_rss () in
+  malloc_stats ();
   let rss_alloc =
     match Sys.getenv_opt "RAWRSS" with
     | None -> Int64.(to_float (sub rss_after rss_before))

This will output something like:

Arena 0:
system bytes     = 1324744704
in use bytes     = 1228628384
Total (incl. mmap):
system bytes     = 1333809152
in use bytes     = 1237692832
max mmap regions =          3
max mmap bytes   =    9064448
1357312 maximum resident set size
15224 RSS overhead

The last system bytes and in use bytes reported remains stable across many runs.
The maximum resident set size changes by about 0.1%:

$ grep 'maximum resident' prog.out.*|cut -f2 -d:|cut -f1 -d ' ' |~/git/ministat/ministat   
x <stdin>
+--------------------------------------------------------------------------------------------------------------------+
|                                                                                                              x     |
|                                                                                                              x     |
|                                                                                                              x     |
|                                                                                                              x     |
|                                                               x                               x              x     |
|                                                               x                               x              x     |
|                                                               x               x               x              x     |
|x                                              x               x               x               x              x     |
|                                                         |____________________________A________M___________________||
+--------------------------------------------------------------------------------------------------------------------+
    N           Min           Max        Median           Avg        Stddev
x  20       1356032       1357824       1357568     1357427.2      473.3177

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment