Memory Management

Levython uses automatic memory management with garbage collection and NaN-boxing for efficient value representation.

NaN-Boxing

All values in Levython are stored as 64-bit NaN-boxed values:

Type Representation Range
Float IEEE 754 double Full double precision
Integer 53-bit signed int -2^53 to 2^53
Boolean Special NaN pattern true/false
Pointer 48-bit pointer in NaN Heap objects

Memory Layout

# Integers - no heap allocation
x <- 42              # 8 bytes on stack

# Strings - heap allocated
name <- "Alice"      # 8 bytes (pointer) + string data on heap

# Lists - heap allocated with capacity
items <- [1, 2, 3]   # 8 bytes (pointer) + 24 bytes header + 24 bytes data

Garbage Collection

Levython uses mark-and-sweep garbage collection:

GC Phases

  1. Mark: Trace from roots and mark reachable objects
  2. Sweep: Free unmarked objects
  3. Compact: Optional heap defragmentation

When GC Runs

  • When heap usage exceeds threshold (default: 8MB)
  • After allocating large objects (>1MB)
  • Manually via gc() function
# Create temporary objects
for i in range(0, 1000) do
    temp <- [i] * 1000  # Large temporary list
end

# GC automatically runs and frees unused memory

Memory-Efficient Patterns

Reuse Objects

# Bad - creates new list each iteration
for i in range(0, 1000) do
    data <- []
    for j in range(0, 100) do
        append(data, j)
    end
    process(data)
end

# Good - reuse same list
data <- []
for i in range(0, 1000) do
    clear(data)  # Clear instead of creating new
    for j in range(0, 100) do
        append(data, j)
    end
    process(data)
end

Avoid Intermediate Lists

# Bad - creates intermediate lists
numbers <- range(0, 1000)
evens <- [x for x in numbers if x % 2 == 0]
doubled <- [x * 2 for x in evens]

# Good - single pass
doubled_evens <- [x * 2 for x in range(0, 1000) if x % 2 == 0]

Use Generators (Coming Soon)

# Future feature: generators for lazy evaluation
# gen fibonacci()
#     a <- 0
#     b <- 1
#     while true do
#         yield a
#         temp <- a
#         a <- b
#         b <- temp + b
#     end
# end

Memory Profiling

Check Memory Usage

# Get current memory usage
before <- memory()
say("Memory before: " + str(before) + " bytes")

# Create some data
big_list <- [0] * 1000000

after <- memory()
say("Memory after: " + str(after) + " bytes")
say("Allocated: " + str(after - before) + " bytes")

Force Garbage Collection

# Create temporary objects
temp <- [0] * 10000000  # 80MB list
temp <- null            # Release reference

# Force GC to reclaim memory
gc()

say("Memory freed")

Object Sizes

Object Type Base Size Per Element
Integer 0 bytes NaN-boxed inline
Float 0 bytes NaN-boxed inline
Boolean 0 bytes NaN-boxed inline
String 24 bytes 1 byte per char
List 24 bytes 8 bytes per element
Function 48 bytes + bytecode size

Memory Leaks Prevention

Common Pitfalls

# Bad - growing list never shrinks
cache <- []
while running do
    data <- fetch_data()
    append(cache, data)  # Cache grows indefinitely
end

# Good - limit cache size
cache <- []
max_size <- 1000
while running do
    data <- fetch_data()
    append(cache, data)
    if len(cache) > max_size then
        remove(cache, 0)  # Remove oldest entry
    end
end

Circular References

Levython's GC handles circular references automatically:

# Circular reference - automatically handled
node1 <- {"value": 1, "next": null}
node2 <- {"value": 2, "next": null}
node1["next"] <- node2
node2["next"] <- node1  # Circular

# When node1 and node2 go out of scope, GC frees both

Best Practices

  • ✅ Reuse objects instead of creating new ones in loops
  • ✅ Limit collection sizes (implement bounded caches)
  • ✅ Clear references when done (obj <- null)
  • ✅ Prefer list comprehensions over manual building
  • ✅ Use gc() after large deallocations
  • ✅ Profile memory usage with memory()
  • ❌ Don't keep references to large temporary objects
  • ❌ Don't create unbounded caches without eviction
  • ❌ Don't ignore memory growth in long-running processes