jonnifyfsharp

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.

C3.01.2 · part 1

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.
C3.01.2 · part 2

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
A practical F# note: large or long-lived allocations land in gen 2 and are collected less often, so a pipeline that churns short-lived values stays in gen 0 where collection is cheapest. The BEAM developer's instinct — "spin up a process, let its heap go when it dies" — has no direct CLR analog; the shared collector reclaims by reachability, not by actor lifetime.
C3.01.2 · part 3

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
This is the axis where the two runtimes feel most different. The BEAM schedules millions of preemptive processes for you; the CLR asks you to model concurrency with 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

F# source let add a b IL · assembly portable JIT → native on first call run · threads shared heap + pool static types managed async / Task
F# source · the compiler checks static types and emits IL. Vs the BEAM: Elixir compiles to BEAM bytecode and resolves types at run time.
Without JavaScript: the four stages, both runtimes
stage CLR (F#) BEAM (Elixir)
F# sourcestatic types checked at compiledynamic, checked at run time
IL · assemblyportable IL, shared by all .NET languagesBEAM bytecode
JIT → nativeJIT-compiled to machine code on first callbytecode interpreted, with JIT on modern BEAM
runthreads + pool, one shared generational GCprocesses, 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

Related in this course