Memory Management & ARC/ORC

Nim 2.x is garbage-collected without a tracing collector: memory is released by reference counting with deterministic destruction, and ORC adds a cycle collector for the structures reference counting alone cannot free. Value types are copied, heap objects are shared, and the compiler removes most copies automatically — this lesson shows where each rule applies.

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.

Value semantics copy the data; reference semantics share one heap cell through 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
  • arc counts references and destroys a value the moment its last owner disappears — usually at the end of the block that created it.
  • orc adds a cycle collector on top: it finds reference cycles (a node pointing back to its parent) that pure counting would leak.
  • refc exists 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

  1. Choose the semantics first: a plain object for a fact, a ref object for shared mutable identity.
  2. Assume a copy on assignment; add move or a sink parameter only where the copy is measured or obvious.
  3. Give every resource-holding type a =destroy that tolerates being called twice and on moved-from values.
  4. Keep --mm:orc unless a benchmark says otherwise; never mix strategies inside one program.
  5. Reach for alloc/create at C boundaries only, and always with a matching defer.
  6. When a hot path surprises you, read the C: nim c --expandArc:procName --compileOnly shows exactly what ARC/ORC inserts.