Memory leaks from compiled functions: Solved

Gesee
Sun, 20 Jul 2025 20:33:53 +0000
Internals & Design

I have been told about one known issue with Julia, compiled function doesn’t get garbage collected. So since I wanted to dynamically create functions for objects in my game engine, it was a big problem and I had to solve it since objects in a game are created and destroyed at an erratic rate.
So I thought about a way to solve that and it’s just to reuse functions.
Yeah cool, but I do we reuse it ?
Simple, we override methods of unused functions.
When we want to reuse a function, we just redefine it so that the old version is overrided by the new one and I made a little module FunctionPooling to automatically solve that.
The result substain my idea. I tried dynamically create 1000 functions.
here is the snippet:

include(joinpath("..","src","FunctionPooling.jl"))

using .FunctionPooling

pool = FunctionPool()

## Warming to avoid compilation overhead.
@pooledfunction pool function update(x::Int)
	x + 5
end

N = 1000 # Number of functions that will be created
total_memory = Sys.total_memory()

## Part 1: Standard Julia version
used_memory = (total_memory - Sys.free_memory())/(1024^2)

# We will generate `N` standard function and make sure they compile by calling them
for i in 1:N
	symb = gensym() # The name doesn't matter
	eval(quote function $symb(x::Int)
		x + 5
	end
    $symb(1)
end)
end

# We call the GC so we free the unused memory.
GC.gc()

used_memory = (total_memory - Sys.free_memory())/(1024^2) - used_memory
println("Memory in use: $used_memory Mo")

## Part 2: Version with reused functions
GC.gc()
used_memory = (total_memory - Sys.free_memory())/(1024^2)

for i in 1:N
	symb = gensym()
	eval(quote @pooledfunction pool function $symb(x::Int)
		x + 5
	end
	$symb(1)
	free($pool,$symb)
end)
end

# We free the unused memory
GC.gc()
used_memory = (total_memory - Sys.free_memory())/(1024^2) - used_memory

println("Memory in use: $used_memory Mo")

And here is the results (It vary but the main point still stand anyway):

# Standard way
Memory in use: 55.5 Mo

# With recycled functions
Memory in use: 9.9 Mo

I think overriding method instance solve that problem.
I’m not sure yet about why it works (just that it seems logical for me that it would work)
Maybe it’s because overriding the function increment the world age and since the old compiled code is in a deprecated world it’s garbage collected, it seems like the most logical explanation to me.

I’m not thinking about making a package for that (FunctionPooling is neat but not my priority so it’s probably not optimal), so if anyone would push that idea further it would be really really cool.

19 posts - 4 participants