Memory Management

How Strada handles memory allocation, references, and cleanup.

Key Concept Strada uses automatic reference counting for most values, with a key optimization: small integers are encoded directly in pointers (tagged integers) with zero heap allocation. You rarely need to think about memory, but understanding the model helps write efficient code.

Tagged Integers

Integers that fit in 63 bits are stored directly inside the pointer itself, not on the heap. The low bit of the pointer is set to indicate a tagged integer. This means integer operations — arithmetic, comparisons, loop counters, array indexing — require no memory allocation and no reference counting. This optimization is completely transparent: all runtime functions like strada_to_int() handle both tagged and heap-allocated integers automatically.

Automatic Reference Counting

Non-integer values (strings, arrays, hashes, references) use reference counting. When a value is assigned to a variable or passed to a function, the count increases. When a variable goes out of scope, the count decreases. When the count reaches zero, the memory is freed.

func example() void {
    my str $s = "hello";   # refcount = 1
    my str $t = $s;         # refcount = 2 (shared)

    # When function returns:
    # $t goes out of scope: refcount = 1
    # $s goes out of scope: refcount = 0 -> freed
}

Inspecting Reference Counts

Use refcount() to see how many references exist to a value:

my array @arr = [1, 2, 3];
say(refcount(\@arr));  # 1

my scalar $ref = \@arr;
say(refcount(\@arr));  # 2

Scope and Lifetime

Variables are scoped to the block where they're declared:

func main() int {
    my str $outer = "outer";

    if (1) {
        my str $inner = "inner";
        say($outer);  # OK: outer is visible
        say($inner);  # OK: inner is visible
    }
    # $inner is freed here

    say($outer);  # OK
    # say($inner);  # Error: $inner not in scope

    return 0;
}

Loop Variable Scope

for (my int $i = 0; $i < 10; $i++) {
    # $i exists here
}
# $i is freed here

foreach my str $item (@items) {
    # $item exists here
}
# $item is freed here

References

References allow multiple variables to point to the same data. Creating a reference increases the refcount:

my array @data = [1, 2, 3];
my scalar $ref = \@data;    # Reference to @data

# Both point to the same array
push(@data, 4);
say($ref->[3]);  # 4

# Array freed only when BOTH go out of scope

Anonymous References

Anonymous arrays and hashes are created with refcount 1:

my scalar $arr = [1, 2, 3];       # Anonymous array ref
my scalar $hash = { "a" => 1 };  # Anonymous hash ref

# Freed when $arr and $hash go out of scope

Object Destruction (DESTROY)

When an object's refcount reaches zero, Strada automatically calls its DESTROY method if one exists:

package Connection;

func new(str $host) scalar {
    my hash %self = {
        "host" => $host,
        "socket" => core::socket_client($host, 80)
    };
    return bless(\%self, "Connection");
}

func DESTROY(scalar $self) void {
    # Called automatically when object is freed
    say("Closing connection to " . $self->{"host"});
    core::close_fd($self->{"socket"});
}

func main() int {
    {
        my scalar $conn = Connection::new("example.com");
        # Use connection...
    }
    # DESTROY called here automatically
    return 0;
}
DESTROY Order With inheritance, DESTROY methods are called from child to parent. Use $self->SUPER::DESTROY() to call parent destructors.

Manual Memory Control

Sometimes you need explicit control over when memory is freed:

Clear a Variable Early

Assign undef to drop a value before its scope ends:

my str $big_string = core::slurp("huge_file.txt");
# Process the string...

$big_string = undef;  # Free memory now, don't wait for scope end

core::release() - Free via Reference

my scalar $ref = [1, 2, 3];
core::release($ref);  # Decrements refcount, $ref becomes undef

Performance Optimization

Pre-allocating Arrays

If you know an array's size in advance, pre-allocate to avoid reallocations:

# Method 1: Declaration with capacity
my array @data[10000];  # Pre-allocate for 10,000 elements

# Method 2: Reserve after creation
my array @items = [];
reserve(@items, 5000);

# Now push() won't need to reallocate
for (my int $i = 0; $i < 5000; $i++) {
    push(@items, $i);
}

Pre-allocating Hashes

# Declaration with capacity
my hash %cache[1000];  # Pre-allocate buckets for ~1000 keys

# Or set default for all new hashes
core::hash_default_capacity(500);

StringBuilder for String Building

String concatenation creates new strings each time. For building large strings, use StringBuilder (the bare sb_new/sb_append/... spellings remain as legacy aliases):

# Inefficient: O(n²) - creates many intermediate strings
my str $result = "";
for (my int $i = 0; $i < 10000; $i++) {
    $result = $result . "line " . $i . "\n";
}

# Efficient: O(n) - appends to buffer
my scalar $sb = sb::new();
for (my int $i = 0; $i < 10000; $i++) {
    sb::append($sb, "line " . $i . "\n");
}
my str $result = sb::to_string($sb);

Array Capacity Functions

Function Description
reserve(@arr, $n)Ensure capacity for at least $n elements

Memory and Closures

Closures capture variables by reference. The captured variables stay alive as long as the closure exists:

func make_counter() scalar {
    my int $count = 0;

    return func () {
        $count++;  # Captures $count by reference
        return $count;
    };
}

my scalar $counter = make_counter();
# $count from make_counter() is kept alive by the closure

say($counter->());  # 1
say($counter->());  # 2

$counter = undef;
# Now $count is finally freed

Closures Without Return Statements

Closures (anonymous functions) that don't have an explicit return statement automatically return undef. This is safe and efficient:

# This closure has no return - it's used as a callback
my scalar $callback = func () {
    say("Processing...");
    do_something();
    # No return statement - automatically returns undef
};

# Safe to call - result is undef
$callback->();

# Common pattern: pass closure to methods
$db->transaction(func () {
    $db->execute("UPDATE ...");
    $db->execute("INSERT ...");
});
Watch for Cycles If a closure captures a variable that references the closure itself, you create a cycle that won't be freed automatically. Break such cycles with core::weaken(), or rely on the automatic cycle collector described below.

Circular References

Plain reference counting cannot free a cycle on its own — every member keeps a sibling alive:

my hash %a = ();
my hash %b = ();
$a{"other"} = \%b;
$b{"other"} = \%a;
# Both have refcount 2 — neither reaches 0 on scope exit

Cycle Collector

By default Strada ships with an automatic cycle collector (a Bacon–Rajan synchronous trial-deletion collector) that reclaims these indirect cycles for you — no core::weaken() required. It runs automatically when enough cycle candidates accumulate, and once more at program exit. Direct, acyclic data is still freed deterministically the instant its refcount hits zero; only genuine cycles wait for the collector.

Notes
  • Enabled by default. Build with ./configure --without-cycle-gc to compile it out (then cycles require core::weaken() as before).
  • Covers cycles through arrays, hashes, and references (the common object-graph case). Cycles that close only through a closure capture still need core::weaken().
  • A cyclic blessed object reclaimed by the collector does not run DESTROY (mirrors Perl's global-destruction behaviour).

You can drive or tune it explicitly when needed:

FunctionDescription
core::gc_collect()Force a cycle collection now
core::gc_disable() / core::gc_enable()Turn automatic collection off / on
core::gc_threshold($n)Set the candidate count that triggers an automatic collection
core::gc_collections()Number of collections run (stats)
core::gc_freed()Number of cyclic objects reclaimed (stats)

Weak References

core::weaken() remains available for cases the collector doesn't cover (closure-captured cycles), for deterministic teardown, or when the collector is configured out.

A weak reference does not keep its target alive. When the target's last strong reference is dropped, the target is freed and all weak references to it become undef. Use core::weaken() to break circular references.

my scalar $parent = { "name" => "parent" };
my scalar $child = { "name" => "child" };
$parent->{"child"} = $child;
$child->{"parent"} = $parent;

core::weaken($child->{"parent"});  # Break the cycle
say(core::isweak($child->{"parent"}));  # 1
say($child->{"parent"}->{"name"});    # "parent" (still accessible)
FunctionDescription
core::weaken($ref)Make $ref a weak reference
core::isweak($ref)Returns 1 if weak, 0 otherwise
Weak Reference Details
  • Works on hash entry values: core::weaken($hash->{"key"})
  • Idempotent: calling core::weaken() on an already-weak ref is a safe no-op
  • Multiple weak references to the same target are supported
  • When the target is freed, dereferencing the weak ref returns undef

Request Arena

For request-scoped workloads (such as a preforking web server handling one request per loop), the request arena bump-allocates values into a region and frees the whole region at once — skipping per-object allocation and refcount teardown for the request's churn.

# Around each unit of request-local work:
core::arena_begin();
handle_request($req);     # allocates freely; nothing freed individually
core::arena_end();         # reclaims everything allocated since begin
FunctionDescription
core::arena_begin()Start a request arena (may be nested)
core::arena_end()Free the current arena wholesale
core::arena_active()Returns 1 if an arena is currently open
Arena rules
  • Enabled by default; build with ./configure --without-arena to compile it out. The arena is dormant until core::arena_begin() is called, so it is safe to ship enabled.
  • Values created inside an arena must not escape arena_end() — storing one into a longer-lived structure leaves a dangling reference.
  • An arena container that references a persistent (pre-arena) value leaks one reference on that value; keep arena data self-contained.
  • DESTROY is not run for objects reclaimed by the arena.
  • Single-threaded scope only (perfect for a preforking worker, which is single-threaded per process).

Memory Profiling

Enable memory profiling to track allocations:

core::memprof_enable();

# ... your code ...

core::memprof_report();  # Print allocation stats
core::memprof_reset();   # Clear counters
core::memprof_disable();

C Interop Memory

When working with C code, you may need manual memory management:

# Allocate C memory
my scalar $ptr = c::alloc(1024);

# Use the memory...
c::write_int32($ptr, 42);

# Free when done
c::free($ptr);

See C Interoperability for more details on working with C memory.

Best Practices

Memory Tips
  • Let automatic cleanup handle most cases - don't over-optimize
  • Assign undef to large objects you're done with early
  • Pre-allocate arrays/hashes when you know the size
  • Use StringBuilder for building large strings
  • Implement DESTROY for objects that hold external resources
  • Avoid circular references, or break them explicitly
  • Use memory profiling to find leaks in long-running programs
  • Use --full-profile for line-level performance profiling (see Debugging)