Memory Management
How Strada handles memory allocation, references, and cleanup.
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;
}
$self->SUPER::DESTROY() to call parent destructors.
Manual Memory Control
Sometimes you need explicit control over when memory is freed:
undef() - Clear a Variable
my str $big_string = slurp("huge_file.txt");
# Process the string...
undef($big_string); # Free memory now, don't wait for scope end
release() - Free via Reference
my scalar $ref = [1, 2, 3];
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 = [];
core::array_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:
# 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 |
|---|---|
core::array_capacity(@arr) | Get current allocated capacity |
core::array_reserve(@arr, $n) | Ensure capacity for at least $n elements |
core::array_shrink(@arr) | Shrink capacity to match current length |
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
undef($counter);
# 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 ...");
});
undef().
Circular References
Reference counting cannot automatically free circular references:
# This creates a cycle that won't be freed!
my hash %a = ();
my hash %b = ();
$a{"other"} = \%b;
$b{"other"} = \%a;
# Both have refcount 2, won't reach 0
# Fix: Break the cycle before scope exit
delete(%a, "other");
# Now %b refcount = 1, %a refcount = 1
# Both freed when scope exits
Weak References
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)
| Function | Description |
|---|---|
core::weaken($ref) | Make $ref a weak reference |
core::isweak($ref) | Returns 1 if weak, 0 otherwise |
- 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
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
- Let automatic cleanup handle most cases - don't over-optimize
- Use
undef()for 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