How Memory Management Works in Nelson: Architecture and Best Practices
Nelson uses a deterministic reference-counting system with copy-on-write semantics, where every variable is an ArrayOf object pointing to a shared Data block that automatically frees memory when the last reference is removed.
Memory management in Nelson is implemented through a sophisticated runtime architecture that balances performance with safety. The nelson-lang/nelson repository employs atomic reference counting and shallow copying strategies to minimize overhead while handling large numerical datasets efficiently. Understanding these internal mechanisms helps developers write memory-efficient code and avoid hidden leaks in long-running sessions.
Core Architecture of Memory Management in Nelson
Reference-Counted Data Blocks
At the heart of Nelson's memory model lies the Data structure defined in modules/types/src/include/Data.hpp. This structure contains the actual raw bytes, dimension information, type classification, and an atomic reference counter named owners.
When a Data block is created, owners initializes to 1. Each time an ArrayOf object copies or references this data, the counter increments via owners.fetch_add(1, std::memory_order_relaxed) in modules/types/src/cpp/Data.cpp. When an ArrayOf is destroyed or reassigned, owners.fetch_sub(1, std::memory_order_acq_rel) decrements the count. Upon reaching zero, the destructor automatically releases the allocated buffer.
The ArrayOf Container
The ArrayOf class in modules/types/src/include/ArrayOf.hpp serves as the high-level container visible to Nelson programmers. It maintains a pointer to a Data block and implements shallow copy semantics by default. This design allows large arrays to be passed between functions or assigned to new variables with minimal overhead, as only the pointer and reference count are copied rather than the underlying bytes.
Copy-on-Write Semantics
Nelson implements copy-on-write (COW) optimization to balance sharing efficiency with data integrity. When multiple ArrayOf instances reference the same Data block, they share memory safely through the reference count. However, when a mutating operation requires exclusive access to the data, Nelson automatically allocates a new Data block, copies the contents, and leaves the original shared block untouched.
This mechanism occurs transparently during operations like indexed assignment or arithmetic modifications, ensuring that changes to one variable do not unexpectedly affect others that were assigned from it.
Memory Management API and Utilities
Monitoring Memory with who and whos
The memory_manager module provides introspection tools to inspect the current workspace. The who function, implemented in modules/memory_manager/src/cpp/Who.cpp, iterates over the current Scope object to list variable names. The whos function in modules/memory_manager/src/cpp/Whos.cpp constructs detailed tables showing variable dimensions, classes, and byte sizes by querying ArrayOf::getByteSize().
Note that sparse matrices report NaN or "-" for byte size in whos output because their compressed storage does not represent a simple contiguous block, as handled in lines 78-84 of Whos.cpp.
Clearing Variables and Freeing Memory
The clear command, defined in modules/memory_manager/src/cpp/Clear.cpp, removes specified variables from the current Scope by calling Scope::deleteVariable. This decrements the reference count of the underlying Data block, potentially freeing memory immediately if no other references exist.
Users can clear specific variables (clear var1 var2), the entire workspace (clear all), or maintain persistent variables while clearing others.
Locking and Persistent Variables
Nelson provides mechanisms to protect variables from accidental deletion. The lockVariable function in modules/memory_manager/src/cpp/LockVariable.cpp marks variables as locked, causing clear operations to skip them. Conversely, unlockVariable removes this protection.
The persistent keyword declares variables that survive function scope across multiple invocations, maintaining their Data blocks in memory throughout the session unless explicitly cleared.
Best Practices for Memory Management in Nelson
-
Explicitly clear large temporaries: Use
clear varnameon intermediate arrays that are no longer needed. This immediately decrements reference counts and releases underlying buffers, preventing invisible memory buildup in long scripts. -
Minimize intermediate copies during modifications: When performing multiple mutations on a large array, avoid creating chains of temporary variables. Each assignment increases reference counts and delays deallocation until all references are cleared.
-
Use persistent variables sparingly: Declare variables
persistentonly when caching data across function calls is essential. Persistent variables remain in memory for the entire session and are excluded fromclear alloperations. -
Lock variables only when necessary: Apply
lockVariableto protect critical data from accidental deletion, but unlock them as soon as protection is no longer required. Locked variables cannot be cleared and may cause memory leaks if forgotten. -
Prefer sparse types for zero-heavy data: Use sparse matrix types when working with data containing many zeros. Nelson reports compressed storage sizes in
whos, indicating reduced memory footprint compared to dense arrays. -
Monitor workspace regularly: Periodically execute
whosormemoryinfoto inspect physical and virtual memory usage. This helps identify unexpectedly large variables or reference accumulation. -
Limit global scope usage: Prefer local variables within functions over global workspace variables. Globals remain reachable throughout the session and cannot be automatically reclaimed when functions exit.
Code Examples
Freeing Large Temporary Variables
% Allocate a large matrix (~200 MB)
A = rand(5000);
% Create a derived array (shallow copy, refcount = 2)
B = A .* 2;
% Remove reference to A
clear A
% Memory remains allocated because B still references the data
% Now free the remaining reference
clear B
% Refcount reaches 0, memory is released immediately
Using Persistent Variables
function out = cachedComputation()
persistent cache
if isempty(cache)
% Expensive initialization happens only once
cache = rand(4000);
end
out = cache * 2;
end
% After multiple calls, 'cache' remains in memory
% It survives 'clear all' unless explicitly cleared within the function
Locking Variables for Protection
% Create important data
myData = rand(3000);
% Prevent accidental deletion
lockVariable('myData');
% This command will NOT free the memory
clear myData
% Explicitly unlock when protection is no longer needed
unlockVariable('myData');
% Now the variable can be cleared
clear myData
Internally, clear myData invokes ClearVariable in modules/memory_manager/src/cpp/Clear.cpp, which calls Scope::deleteVariable. The lockVariable and unlockVariable functions are implemented in modules/memory_manager/src/cpp/LockVariable.cpp.
Key Source Files
| File | Purpose |
|---|---|
modules/types/src/include/Data.hpp |
Defines the reference-counted Data structure with atomic owners counter |
modules/types/src/cpp/Data.cpp |
Implements reference count increment/decrement operations using fetch_add and fetch_sub |
modules/types/src/include/ArrayOf.hpp |
High-level container implementing shallow copies and copy-on-write semantics |
modules/memory_manager/src/cpp/Who.cpp |
Lists variable names from the current scope |
modules/memory_manager/src/cpp/Whos.cpp |
Displays detailed variable information including byte sizes |
modules/memory_manager/src/cpp/Clear.cpp |
Implements variable removal and reference count decrementing |
modules/memory_manager/src/cpp/LockVariable.cpp |
Handles variable locking to prevent deletion |
modules/memory_manager/src/cpp/MemoryInformation.cpp |
Platform-specific system memory statistics |
Summary
- Nelson employs reference counting via atomic
ownerscounters inDatablocks, eliminating the need for garbage collection while ensuring deterministic memory release. - The copy-on-write pattern allows efficient sharing of large arrays between variables through shallow copies in
ArrayOfcontainers. - Explicit memory management commands (
clear,lockVariable,unlockVariable,persistent) provide fine-grained control over variable lifetimes. - Monitoring tools (
who,whos,memoryinfo) expose internal memory usage and reference status to help optimize resource consumption. - Following best practices—such as clearing large temporaries, minimizing persistent variables, and using sparse types—prevents memory leaks and maintains performance in long-running Nelson sessions.
Frequently Asked Questions
How does Nelson handle memory deallocation without a garbage collector?
Nelson uses deterministic reference counting where every Data block tracks active references through an atomic owners counter. When ArrayOf objects are destroyed or reassigned, they decrement this counter using owners.fetch_sub(1, std::memory_order_acq_rel). Once the count reaches zero, the Data destructor immediately releases the raw buffer, ensuring memory is freed at predictable moments without garbage collection overhead.
What is copy-on-write in Nelson and when does it trigger?
Copy-on-write (COW) is an optimization that allows multiple ArrayOf variables to share the same underlying Data block through shallow copies. The shared data remains read-only across all references. When any variable attempts to modify the data—such as through indexed assignment or arithmetic operations—Nelson automatically allocates a new Data block, copies the contents, and applies the mutation to the exclusive copy, leaving the original shared block untouched for other references.
Why do sparse matrices show NaN or "-" for byte size in the whos command?
Sparse matrices use compressed storage formats that do not allocate a contiguous block of memory proportional to their dimensions. Because the actual memory footprint depends on the number of non-zero elements and internal compression structures, ArrayOf::getByteSize() cannot return a simple byte calculation. The whos implementation in modules/memory_manager/src/cpp/Whos.cpp specifically checks for sparse types and displays "-" or NaN to indicate that the storage is compressed rather than dense.
How can I prevent a variable from being accidentally cleared in Nelson?
Use the lockVariable function to protect critical data from deletion. When a variable is locked, the clear command skips it during cleanup operations, as implemented in modules/memory_manager/src/cpp/LockVariable.cpp. The variable remains accessible and modifiable, but cannot be removed from the workspace until you explicitly call unlockVariable. This is particularly useful for protecting large baseline datasets or configuration constants during complex interactive sessions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →