Memory Management & ARC/ORC
Values, References and Copies
The first question about any value in Nim is whether you hold the data or a handle to it. Objects, tuples, arrays, sequences and strings are values; ref and ptr are handles.
Figure 1 — copying a value duplicates storage; copying a reference duplicates only the handle, so both names see the same heap cell.
Value Semantics by Default
type
Point = object
x, y: int
var a = Point(x: 1, y: 2)
var b = a # a full value copy: 'b' owns its own ints
b.x = 99 # mutate the copy only
echo a.x # 1 — the original is untouched
echo b.x # 99
var names = @["ada", "grace"]
var alias = names # sequences are values too: this copies
alias.add("linus")
echo names.len # 2 — the copy grew, not the original
Reference Objects and Identity
type
Counter = ref object # 'ref object' lives on the heap
value: int
var shared = Counter(value: 0)
var handle = shared # copies the HANDLE: both names see one object
handle.value += 1
echo shared.value # 1 — one object, two names
if shared.isNil: # references can be nil; check before use
echo "no counter"
echo handle.value # 1
Use ref when several parts of the program must observe the same object — a graph node, a cache entry, a mutable model. Use a plain object when the value is a fact: coordinates, a result, a configuration.
Transfers with sink and move
proc consume(data: sink seq[int]): int =
## 'sink' declares that this procedure takes ownership of the sequence
## instead of borrowing it, so the caller may hand over the buffer.
data.len # the callee may keep or free it
var numbers = @[1, 2, 3, 4]
echo consume(move(numbers)) # 4 — no copy: the buffer changes owner
echo numbers.len # 0 — 'move' resets the source; reuse it only after
# you assign a new value
# Without 'move', passing a named variable may still copy, because the variable
# is used later. A temporary is transferred with no syntax at all:
echo consume(@[5, 6]) # 2 — the temporary was already owned by nobody
move is not a cast and not unsafe: it tells the compiler the source will not be used again, which converts a copy into a pointer hand-off. Use it when a large buffer is finished with, and expect a cleared source afterwards.
Management Strategies
The strategy is chosen when you compile, not when you write. Your code keeps the same types; only who frees what changes.
ARC and ORC
nim c --mm:orc main.nim # default in Nim 2.x: reference counting + cycle collector
nim c --mm:arc main.nim # counting only: no cycle collector, lowest overhead
nim c --mm:refc main.nim # the legacy tracing GC of Nim 1.x
nim c --mm:none main.nim # no automatic management: manual alloc/dealloc only
arccounts references and destroys a value the moment its last owner disappears — usually at the end of the block that created it.orcadds a cycle collector on top: it finds reference cycles (a node pointing back to its parent) that pure counting would leak.refcexists for legacy code and for measured cases; new code should not start there.
Destructor Hooks
type
Handle = object
id: int # only trivially copyable fields, so the hook is the
isOpen: bool # the only thing that needs any special care
proc openHandle(id: int): Handle =
echo "acquire resource ", id
Handle(id: id, isOpen: true)
proc `=destroy`(h: var Handle) =
## Called automatically when a Handle goes out of scope, and also for
## moved-from values — hence the guard.
if h.isOpen:
echo "release resource ", h.id
h.isOpen = false
proc work() =
let h = openHandle(7) # acquire
echo "working with ", h.id
# release happens here, at the end of 'work'
work() # acquire resource 7
# working with 7
# release resource 7
Deterministic destruction is the payoff: file handles, sockets, locks and buffers are released at a predictable point, even when an exception unwinds the stack. The related hooks are =copy (custom duplication), =sink (custom transfer) and =trace (which fields ORC's cycle collector must follow); wasMoved marks a value as consumed inside a hook that takes a var parameter.
Manual Allocation
proc histogram(count: int): ptr UncheckedArray[byte] =
## alloc0 returns a zero-filled block; nothing frees it for you.
result = cast[ptr UncheckedArray[byte]](alloc0(count))
let cells = histogram(4)
cells[0] = 9 # no bounds check: 'Unchecked' is a promise, not a guard
echo cells[0] # 9
dealloc(cells) # exactly one dealloc per alloc
# Typed heap allocation with automatic cleanup of the value's own fields:
proc counted(): int =
let p = create(int) # create(T): allocate and initialize a T
defer: destroy(p) # the value's destructor runs, then memory is freed
p[] = 42
p[] + 1
echo counted() # 43
alloc, alloc0, allocShared, create and createShared bypass the reference-counting machinery: the block is yours until dealloc, deallocShared or destroy. Prefer ref objects; reach for raw pointers at C boundaries and in allocators, and pair every allocation with a defer in the same procedure.
Memory in Practice
Nim moves or elides most copies by itself. The remaining work is to give it the information it needs: views instead of values, ownership markers instead of guesses, and a look at the generated C when the numbers matter.
Borrowing Instead of Copying
type
Person = object
name: string
age: int
proc total(items: openArray[int]): int =
## 'openArray' is a view: no copy, and it accepts arrays, seqs and slices.
for item in items:
result += item
proc nameOf(p: Person): lent string =
## 'lent' returns a borrowed view of a field: no string copy, no ownership.
p.name
let people = [Person(name: "Ada", age: 36)]
echo total([1, 2, 3]) # 6 — an array, borrowed with no allocation
echo total(@[4, 5]) # 9 — the sequence's buffer is borrowed too
echo nameOf(people[0]) # Ada — a read-only view into 'people'
The cursor pragma completes the toolbox: attach it where a read-only routine would otherwise pay reference-count traffic for a parameter, and the compiler stops emitting those calls.
Review Checklist
- Choose the semantics first: a plain object for a fact, a
refobject for shared mutable identity. - Assume a copy on assignment; add
moveor asinkparameter only where the copy is measured or obvious. - Give every resource-holding type a
=destroythat tolerates being called twice and on moved-from values. - Keep
--mm:orcunless a benchmark says otherwise; never mix strategies inside one program. - Reach for
alloc/createat C boundaries only, and always with a matchingdefer. - When a hot path surprises you, read the C:
nim c --expandArc:procName --compileOnlyshows exactly what ARC/ORC inserts.