Skip to content

Commit c390c3e

Browse files
committed
Fix bug with GC and function lifetime in the cache
Functions with bodies which share identical syntax collide in the cache. The cache needs to be carefully managed to ensure this isn't a problem. In particular, if a cache entry already exists for a given `id`, we need to use that as the body rather than the body we have. But only if it wasn't previously collected.
1 parent ac079dd commit c390c3e

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)