Dive C3.01.2
The CLR — managed execution
A managed runtime for many languages, with one shared heap and one shared scheduler under the OS.
F# runs on the .NET Common Language Runtime. Read on the same four axes as the
BEAM, it answers each differently. Code does not run as native instructions directly: F# compiles to a
common intermediate language, which the CLR's JIT compiler turns into
machine code as methods first run. Memory is reclaimed by one shared generational garbage
collector for the whole process — not a collector per anything. The concurrency unit is the
OS thread, and because threads are comparatively expensive, .NET multiplexes work over a
thread pool; async and Task let many
logical operations share few threads. And the runtime enforces a common type system — F#'s
static types are checked once at compile time and again, structurally, by the CLR. F#'s actor is the
MailboxProcessor, one object on this shared runtime.
Source → IL → JIT → machine code
"Managed" means the runtime, not your code, owns execution and memory. The F# compiler emits IL — a portable instruction set — into an assembly. At run time the CLR's JIT compiles each method to native code the first time it is called, then caches it. The same runtime hosts C#, F#, and VB, which is why F# can call any .NET library: they all compile to the one IL and share the one type system. The BEAM is also a managed VM, but it executes its own bytecode and is built around the process model rather than a shared heap.
// F# — compiles to IL, JIT-compiled by the CLR on first call let add a b = a + b // val add: a: int -> b: int -> int add 20 22 // val it : int = 42
- CLR
- the Common Language Runtime: the managed virtual machine that loads assemblies, JIT-compiles IL, and runs it.
- IL
- intermediate language — the portable instruction set every .NET language compiles to; the shared layer that makes interop work.
- JIT
- just-in-time compilation: IL becomes native machine code as methods first execute, then is cached.
- type system
- the common type system the runtime enforces; F#'s static types live on top of it and are checked at compile time.
One generational heap, not one per process
The CLR collects garbage with a generational collector over a heap shared by the whole process. Objects are allocated in generation 0; those that survive a collection are promoted to gen 1, then gen 2, on the observation that most objects die young. The trade is the mirror of the BEAM's: the BEAM gives each process its own small heap, so a collection touches one process; the CLR keeps one heap, so a collection reasons about the whole program at once. There is no per-actor heap to reclaim independently.
One consequence shows up in the type system. The CLR distinguishes value types (such as
int, allocated inline, often on the stack) from reference types
(such as string and records, allocated on the managed heap and tracked by the GC).
Which one a value is decides whether it is garbage to collect — a distinction the BEAM does not surface to
the programmer.
// F# — value types vs reference types (what the GC tracks) typeof<int>.IsValueType // val it : bool = true — inline, not GC-tracked typeof<string>.IsValueType // val it : bool = false — heap, GC-tracked
Threads, the pool, and async
The CLR's concurrency unit is the OS thread, scheduled by the operating system. A thread is far heavier
than a BEAM process, so creating one per logical task does not scale the way spawning processes does.
Instead .NET keeps a thread pool and runs short work items on it, and F#'s
async (and the underlying Task) lets many logical
operations share a small number of threads: an async that awaits I/O
releases its thread back to the pool until the result arrives. The model is cooperative, not preemptive —
progress happens at await/let! points.
// F# — async over the thread pool let work () = async { let! a = async { return 20 } let! b = async { return 22 } return a + b } work () |> Async.RunSynchronously // val it : int = 42
async /
Task over a bounded pool. The MailboxProcessor wraps this
into an actor — a mailbox and a receive loop — but it is one object on the shared runtime, not a supervised
process.Trace a value through the CLR
Select a stage. The diagram marks where execution is; the readout names what the CLR does at that stage and how it differs from the BEAM's answer on the same axis. The pipeline is the everyday path of F# code: compile to IL, JIT to native, allocate on the shared heap, and run concurrency over threads.
The managed pipeline · select a stage
| stage | CLR (F#) | BEAM (Elixir) |
|---|---|---|
| F# source | static types checked at compile | dynamic, checked at run time |
| IL · assembly | portable IL, shared by all .NET languages | BEAM bytecode |
| JIT → native | JIT-compiled to machine code on first call | bytecode interpreted, with JIT on modern BEAM |
| run | threads + pool, one shared generational GC | processes, GC per process |
The CLR owns one heap, one set of threads, and one type system for the whole process. The BEAM
owns many small heaps and many cheap processes. F# is at home on the first — and the
MailboxProcessor is how it draws an actor on it.
Where this leads
Both runtimes are now named on the same four axes. The last dive reads them together — what carries over from the BEAM unchanged, what F# gains from the type system, and the one capability, OTP-style supervision, that the CLR leaves to the program to build.
References
Sources
- Microsoft — .NET documentation — the CLR, managed execution, the JIT, generational garbage collection, and the threading model.
- Microsoft — F# documentation —
asyncworkflows and value vs reference types in F#. - FSharp.Core API reference — the
Asyncmodule andMailboxProcessor. - The F# Software Foundation — F# on the .NET runtime.
Related in this course
- C0.2 — The .NET runtime & the open-source turn — the CLR's history and the common type system, in depth.
- C3.01.1 — The BEAM you know — the same four axes, the process VM.
- Elixir — The BEAM's evolution — the runtime contrasted here.