OCaml Module 13
Runtime, performance and interoperability interview questions
Interview questions on runtime, performance and interoperability: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
Which OCaml values cost no allocation, and which live in a block?
Answered in the lesson Memory representation and the garbage collector
-
Fresher to mid
What does dune's (modes byte exe js wasm) stanza do, and when would you shorten it?
Answered in the lesson OCaml in the browser: js_of_ocaml and Wasm
-
Fresher to mid
What does #trace print, and when is it the quickest way to find a bug?
Answered in the lesson Debugging, profiling and benchmarking
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to staff
A booking service on OCaml 5.5 builds a response tree of about 16,000 words for each request and drops it after the reply. With one request in flight the collector is quiet; with 32 in flight the major heap grows fivefold and the slowest responses stall. Explain what the collector is doing, and what you would change.
- What it tests
- Knowing that a minor collection copies whatever is still reachable, seeing that requests in flight keep young data alive, and choosing between a larger minor heap and less allocation, with numbers.
- First step
- Measure promoted words per request at both loads, with Gc.quick_stat read before and after a fixed number of requests. If they jump from a small share of the tree to almost all of it, the trees are still in flight when the minor heap fills, so each one is copied into the major heap and has to be collected there later.
- Follow-ups
-
- Why does the same code promote more at a higher load?
- What does a larger minor heap change, and what does it cost?
- Would lowering space_overhead help here?
- How would you confirm that the stalls come from the collector?
From the lesson Memory representation and the garbage collector
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A pricing service keeps 2 million quotes in a Hashtbl keyed by 11-character SKU strings, with type quote = { price : float; qty : int; updated : float }. The team budgeted 64 bytes an entry, and the process holds far more. Work out the words one entry really takes, and cut them without changing any lookup.
- What it tests
- Counting words from the representation of strings, records that mix floats and ints, boxed floats and hash table buckets, then choosing a layout that removes the boxes.
- First step
- Count one entry on paper, then check the count with Obj.reachable_words over the whole table divided by its length. The 11-byte key is a 3-word string, and the record mixes floats with an int, so it is a 4-word block whose two floats sit in boxes of their own; the table adds a bucket cell for every binding.
- Follow-ups
-
- Where do the words beyond the record and the key come from?
- Which change removes the float boxes, and what does it cost?
- Does creating the table at its final size save memory?
From the lesson Memory representation and the garbage collector
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A hand-written C stub returns a record built with caml_alloc. Under load it sometimes returns garbage or crashes, but never in small tests. What rule is most likely broken, and how would you find and fix it?
- What it tests
- Knowing that an allocation can move young values, that every value held across one must be a registered root, and that the bug surfaces only when a collection falls between two allocations.
- First step
- Reproduce it on purpose: call the stub in a tight loop that allocates hard between calls so a minor collection lands inside the stub. Then read the stub for any OCaml value kept across an allocation without CAMLparam or CAMLlocal.
- Follow-ups
-
- Why does the bug appear under load but not in a unit test?
- What does Store_field do that a plain field assignment does not?
- When is it safe to omit CAMLparam and CAMLlocal entirely?
From the lesson Interfacing with C
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A colleague marks a numeric stub [@@noalloc] to speed it up, but the stub calls caml_copy_double to return a float. The program passes every test, then crashes in production. Explain the connection.
- What it tests
- Seeing that [@@noalloc] is a promise the compiler optimises on, that caml_copy_double allocates, and that the broken promise lets a collection run without the bookkeeping the compiler skipped.
- First step
- State what [@@noalloc] means: the C function does not allocate, raise or release the domain lock, so the native compiler skips the bookkeeping it puts around a C call. Then note that caml_copy_double allocates, so a collection can run mid-call without that bookkeeping.
- Follow-ups
-
- Why is there no compiler error or warning for this mistake?
- How would you return a float from a stub that must stay no-alloc?
- What do [@untagged] and [@unboxed] promise, and how do they differ from [@@noalloc]?
From the lesson Interfacing with C
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to staff
Your team must bind a C library of about forty functions, mostly taking ints, doubles and pointers, to OCaml. Would you write hand stubs or use ctypes, and what decides it?
- What it tests
- Weighing per-function C effort against a dependency on ctypes and libffi, the kinds of values crossing the boundary, and call overhead, rather than a blanket preference for either.
- First step
- List what crosses the boundary: for ints, doubles and pointers ctypes removes forty stubs and is a natural fit; for OCaml strings, arrays or records a hand stub and the CAML macros are safer. Then check whether the build may depend on ctypes and libffi.
- Follow-ups
-
- When does ctypes stub generation beat its dynamic Foreign.foreign path?
- Which functions of the library, if any, would you still write by hand?
- How do OCaml 5.5 external types help describe the library's opaque types?
From the lesson Interfacing with C
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A browser project binds eight DOM APIs through Js.Unsafe.get and Js.Unsafe.meth_call. Three of the eight are already in Dom_html with the correct type. A teammate says: rewrite all eight to use Js.Unsafe for consistency. What is the better rule, and why?
- What it tests
- Knowing the two layers of js_of_ocaml bindings and choosing them per API, keeping the type checker useful where it already covers an API, and knowing when the ppx is needed and when it is not.
- First step
- Rewrite only the five APIs that Dom_html does not cover to use Js.Unsafe. Keep Dom_html.createButton, Dom_html.window and the typed functions as they are, so the compiler keeps checking method names, argument types and return types for them. Add a comment next to each Js.Unsafe call that names the browser API it reaches, so the next reader knows why it is at the lower layer.
- Follow-ups
-
- What does the teammate have in mind by "consistency", and what does it cost here?
- When would you add js_of_ocaml-ppx to the switch and rewrite the typed layer with ##.?
- How do you keep a typo in a Js.Unsafe method name from reaching production?
From the lesson OCaml in the browser: js_of_ocaml and Wasm
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A team pushes a 2.2 MB JavaScript bundle to a user-facing site because that is what dune build emits. The lead says: "the gzipped size is 500 KB, so a browser only downloads 500 KB." Explain what is wrong with that reasoning, and give the one-line fix.
- What it tests
- Knowing the dev-release gap, how dune profiles work, and why raw and parsed bytes also matter to a browser, not only the number on the wire.
- First step
- Rebuild with dune build --profile release (dune build -p PACKAGE for opam). The same source compiles to 72,471 bytes raw and 22,065 gzipped: thirty times smaller than the dev build the team shipped. Even compressed, the dev file is about 23 times larger (503,257 bytes). Parsing time scales with the raw size, not the gzip size, so the browser also uses less CPU after download.
- Follow-ups
-
- How did the team end up shipping the dev build?
- What checks would stop a dev build from reaching the user-facing site next time?
- How do you measure the parse-and-execute time, not just the download size?
From the lesson OCaml in the browser: js_of_ocaml and Wasm
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A single-file dom_counter.ml holds every piece of a running browser program: the counter state, the arithmetic, the formatting and every DOM call. Six months later a native CLI is wanted that reports the same counter from a saved file. How do you refactor without breaking the browser version, and what do you test first?
- What it tests
- Splitting pure logic from the browser front end, naming the library and executable layout dune supports, writing native tests that cover the library, and leaving a thin front end for browser-only concerns.
- First step
- Move the arithmetic and formatting into a counter.ml library with no js_of_ocaml dependency. The browser program becomes main_browser.ml with (modes js) and (libraries counter js_of_ocaml), and the new CLI becomes main_cli.ml with (libraries counter). Write Alcotest cases against Counter.increment and Counter.format in a test/ folder first; both executables then share the same tested bytes.
- Follow-ups
-
- Which parts of the browser program must stay in the front-end binary, and why?
- What would make a test case fail on one back end but pass on another?
- How do you avoid an int-size trap when the counter grows beyond 2^31?
From the lesson OCaml in the browser: js_of_ocaml and Wasm
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A nightly OCaml job that summarises 2 million log lines took 4 minutes last month and takes 40 minutes since a refactor, with the same output. Nobody knows which function got slower. How do you find it, and how do you show that your fix worked?
- What it tests
- Treating a correct but slow program as a profiling question, choosing between exact counts and samples, and proving the fix with a fair comparison rather than one timing.
- First step
- Reproduce the slowdown on an input small enough to run in seconds, then profile the old and the new build in the same way, with exact call counts or a sampling profiler, and compare which function's share grew before changing any code.
- Follow-ups
-
- Why profile on a smaller input, and what can that hide?
- Counts or samples: which do you trust for this question?
- How do you show the fix worked without fooling yourself?
From the lesson Debugging, profiling and benchmarking
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to staff
After a parser rewrite, a service's minor collections rise from 50 to 400 a second at the same traffic, and its p99 latency doubles. How do you find which code allocates the extra words, without guessing?
- What it tests
- Measuring allocation per request before attributing it, using an allocation profiler to name the call sites, and cutting the allocation where it is made.
- First step
- Count the minor words per request in both versions with Gc.minor_words around a fixed batch of requests, so the size of the change is a number. Then sample the new version's allocations with Gc.Memprof and add the samples up by call site.
- Follow-ups
-
- Which sampling rate do you choose, and what does it cost?
- Why are allocation samples steadier than timings?
- Once you know the call site, how do you cut the allocation?
From the lesson Debugging, profiling and benchmarking
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate's change claims to make the price formatter 15% faster, from one run of each version timed with Unix.gettimeofday on a laptop. What do you ask for before you accept the claim?
- What it tests
- Knowing what makes a timing comparison fair, and which measurements stay the same from one run to the next.
- First step
- Ask how the two numbers were produced: the machine, the compiler and its flags, the input, and how many runs of each version stand behind each number.
- Follow-ups
-
- Why is one run of each version not evidence?
- What is wrong with Unix.gettimeofday for this?
- The medians differ by 3% and the ranges overlap. What do you report?
- The build used -O3. Does that matter?
From the lesson Debugging, profiling and benchmarking
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes