Skip to content

Commit 3741e0b

Browse files
Merge pull request #10 from c42f/cjf/gc-fix
Fix bug with GC and function lifetime in the cache
2 parents ac079dd + c390c3e commit 3741e0b

File tree

2 files changed

+41
-16
lines changed

2 files changed

+41
-16
lines changed

src/RuntimeGeneratedFunctions.jl

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ export @RuntimeGeneratedFunction
1010
1111
This type should be constructed via the macro @RuntimeGeneratedFunction.
1212
"""
13-
mutable struct RuntimeGeneratedFunction{moduletag,id,argnames}
13+
struct RuntimeGeneratedFunction{moduletag,id,argnames}
1414
body::Expr
1515
function RuntimeGeneratedFunction(moduletag, ex)
16-
id = expr2bytes(ex)
1716
def = splitdef(ex)
1817
args, body = normalize_args(def[:args]), def[:body]
19-
f = new{moduletag,id,Tuple(args)}(body)
20-
_cache_self(moduletag, id, f)
21-
return f
18+
id = expr2bytes(body)
19+
cached_body = _cache_body(moduletag, id, body)
20+
new{moduletag,id,Tuple(args)}(cached_body)
2221
end
2322
end
2423

@@ -62,18 +61,20 @@ end
6261

6362
@inline @generated function generated_callfunc(f::RuntimeGeneratedFunction{moduletag, id, argnames}, __args...) where {moduletag,id,argnames}
6463
setup = (:($(argnames[i]) = @inbounds __args[$i]) for i in 1:length(argnames))
65-
f_value = _lookup_self(moduletag, id)
64+
body = _lookup_body(moduletag, id)
65+
@assert body !== nothing
6666
quote
6767
$(setup...)
68-
$(f_value.body)
68+
$(body)
6969
end
7070
end
7171

72-
### Function caching and lookup
72+
### Body caching and lookup
7373
#
74-
# Looking up a RuntimeGeneratedFunction based on the id is a little complicated
75-
# because we want the `id=>func` mapping to survive precompilation. This means
76-
# we need to store the mapping created by a module in that module itself.
74+
# Looking up the body of a RuntimeGeneratedFunction based on the id is a little
75+
# complicated because we want the `id=>body` mapping to survive precompilation.
76+
# This means we need to store the mapping created by a module in that module
77+
# itself.
7778
#
7879
# For that, we need a way to lookup the correct module from an instance of
7980
# RuntimeGeneratedFunction. Modules can't be type parameters, but we can use
@@ -86,13 +87,29 @@ end
8687
_cachename = Symbol("#_RuntimeGeneratedFunctions_cache")
8788
_tagname = Symbol("#_RuntimeGeneratedFunctions_ModTag")
8889

89-
function _cache_self(moduletag, id, f)
90-
# Use a WeakRef to allow `f` to be garbage collected. (After GC the cache
91-
# will still contain an empty entry with key `id`.)
92-
getfield(parentmodule(moduletag), _cachename)[id] = WeakRef(f)
90+
function _cache_body(moduletag, id, body)
91+
cache = getfield(parentmodule(moduletag), _cachename)
92+
# Caching is tricky when `id` is the same for different AST instances:
93+
#
94+
# Tricky case #1: If a function body with the same `id` was cached
95+
# previously, we need to use that older instance of the body AST as the
96+
# canonical one rather than `body`. This ensures the lifetime of the
97+
# body in the cache will always cover the lifetime of the parent
98+
# `RuntimeGeneratedFunction`s when they share the same `id`.
99+
#
100+
# Tricky case #2: Unless we hold a separate reference to
101+
# `cache[id].value`, the GC can collect it (causing it to become
102+
# `nothing`). So root it in a local variable first.
103+
#
104+
cached_body = haskey(cache, id) ? cache[id].value : nothing
105+
cached_body = cached_body !== nothing ? cached_body : body
106+
# Use a WeakRef to allow `body` to be garbage collected. (After GC, the
107+
# cache will still contain an empty entry with key `id`.)
108+
cache[id] = WeakRef(cached_body)
109+
return cached_body
93110
end
94111

95-
function _lookup_self(moduletag, id)
112+
function _lookup_body(moduletag, id)
96113
getfield(parentmodule(moduletag), _cachename)[id].value
97114
end
98115

test/runtests.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,11 @@ using RGFPrecompTest
8080

8181
@test RGFPrecompTest.f(1,2) == 3
8282

83+
# Test that RuntimeGeneratedFunction with identical body expressions (but
84+
# allocated separately) don't clobber each other when one is GC'd.
85+
f_gc = @RuntimeGeneratedFunction(Base.remove_linenums!(:((x,y)->x+y+100001)))
86+
let
87+
@RuntimeGeneratedFunction(Base.remove_linenums!(:((x,y)->x+y+100001)))
88+
end
89+
GC.gc()
90+
@test f_gc(1,-1) == 100001

0 commit comments

Comments
 (0)