Why Nim 2.0 Defaults to ORC: Deterministic Memory Without a Tracing GC
Nim 2.0 defaults to ORC for deterministic memory management without a tracing GC. Learn how ARC and ORC work, when to choose each mode, and what trade-offs to expect.
19 Apr 2026, 22:30 UTC

The problem: GC pauses don't fit every target
Tracing garbage collectors simplify application code but introduce unpredictable latency and require a runtime that isn't available on bare-metal, WebAssembly, or constrained embedded targets. Nim 1.4 introduced ARC (Automatic Reference Counting) and ORC (Optimized Reference Counting) as compile-time alternatives. Since Nim 2.0, --mm:orc is the default—a pragmatic engineering decision that trades a modest binary-size increase for deterministic destruction and no GC runtime.
How ARC and ORC differ from a tracing GC
ARC inserts retain/release calls at compile time based on ownership analysis, and defers destructors to scope exit. This gives you RAII semantics similar to C++ or Rust without a background collector. ORC adds a lightweight cycle collector that runs periodically or on demand, reclaiming reference cycles (doubly-linked lists, observer graphs) without requiring pervasive weak annotations. Both operate on thread-local heaps; cross-thread sharing requires explicit shared annotations and atomic reference counts.
Why the default matters for systems work
With --mm:arc or --mm:orc, Nim compiles to targets that have no OS runtime: --os:standalone for bare metal, --target:wasi for WebAssembly, or microcontroller toolchains. Objects have predictable lifetimes, so passing pointers to C/C++ libraries doesn't require pinning or GC-root registration—though you must ensure C code doesn't store Nim pointers beyond the Nim object's scope. The borrow-checker-like lent/owned annotations (still evolving) add compile-time safety for aliasing without runtime cost.
Worked example: a cyclic doubly-linked list under ORC
Create a minimal project to see ORC collect a cycle automatically:
# list.nim
import std/memory
type
Node* = ref object
value: int
prev, next: Node
proc newNode(v: int): Node =
new(Node, value: v)
proc link(a, b: Node) =
a.next = b
b.prev = a
when isMainModule:
var head = newNode(1)
var tail = newNode(2)
link(head, tail)
link(tail, head) # creates a cycle
# head and tail go out of scope here
echo "Scope exited; ORC will collect the cycle on next collection"
GC_collect() # force collection for demo
echo "Collection done"
Build and run with cycle-collector logging:
nim c --mm:orc --debug:gc list.nim
./list
You should see debug output indicating the cycle collector ran and freed the two nodes. With --mm:arc (no cycle collector) the same program would leak the cycle until process exit. With --mm:refc (legacy deferred-reference-counting GC) the tracing collector would eventually reclaim it, but at non-deterministic times.
Trade-offs you should weigh
- Binary size:
--mm:arcbinaries are typically 10–20% larger than--mm:refcequivalents because of inserted retain/release calls. - Compile time: ownership analysis adds front-end work; large codebases see noticeably longer compiles.
- Cycle collector pauses: ORC's collection scales with reachable objects in cycles—usually sub-millisecond, but not hard real-time.
- Thread sharing complexity:
sharedobjects use atomic refcounts; highly concurrent designs need careful architecture. - Ecosystem friction: some Nimble packages still assume
--mm:refcsemantics; migration may require forks or upstream fixes. - Leak debugging: a missing
dealloccan be far from the leak site;--memAnalysishelps but isn't a full leak detector.
How to decide for your project
- Start with the default
--mm:orcfor applications, CLIs, and services where deterministic latency and lower memory overhead matter. - Use
--mm:arcfor bare-metal, WASI, or kernels where even the cycle collector's runtime is unwanted. - Stick with
--mm:refcor--mm:boehmonly if you depend on libraries that haven't been ported or if you need the mature tracing GC for very large heaps with complex object graphs. - Verify: build your representative workload with each
--mmoption, measure binary size, RSS, and tail latency. Runkoch testswith your chosen mode to catch stdlib regressions.
The Nim 2.0 default reflects a bet that most systems code benefits more from predictable destruction than from the throughput of a tracing collector. If your target lacks a runtime or your latency budget is tight, ORC is a practical, well-supported choice—just budget for the compile-time and binary-size costs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.