Hi All,
I’m trying out something in an attempt to make an as unintrusive as possible threadsafe cache type.
The basic idea is that upon creation it will generate as many caches with the initial value as there are threads. Then whenever anything acts on the cache, it will automatically act on the cache with the respective Threads.threadid(), thus making actions on it completely threadsafe.
It looks something like:
struct ThreadCache{T}
caches::Vector{T}
ThreadCache(orig::T) where {T} = new{T}([deepcopy(orig) for i = 1:nthreads()])
end
then currently I defined manually the functions I was often performing on the caches inside the threaded loops along the lines of:
@inline cache(t::ThreadCache) = t.caches[threadid()]
+(t::ThreadCache{T}, v::T) where T = cache(t) + v
-(t::ThreadCache{T}, v::T) where T = cache(t) - v
*(t::ThreadCache{T}, v::T) where T = cache(t) * v
/(t::ThreadCache{T}, v::T) where T = cache(t) / v
etc
Now for what I’m currently using this works fine, but I can’t help but feel that this could be extended very easily to accomodate anything if only I am able to specify that anything (like methodswith) that would act on cached type T like f(...., cached::T,....) would work on the cache like f(...., th::ThreadCache{T}, ....) = f(...., cache(th), ....) calling the previous function.
This feels like something that shouldn’t be too hard but I can’t seem to figure it out in a versatile way.
Yes something like that, but with t being at any place in the arguments. Anyway indeed I think it would need some metaprogramming magic but Idk if it’s even at all possible with that.
using Base.Threads: nthreads, threadid
struct ThreadCache{T}
caches::Vector{T}
ThreadCache(orig::T) where {T} = new{T}([deepcopy(orig) for i = 1:nthreads()])
end
@inline cache(t::ThreadCache) = t.caches[threadid()]
threadlocal(x) = x
threadlocal(x::ThreadCache) = cache(x)
macro threadlocal(expr)
@assert expr.head == :call
f = esc(expr.args[1])
args = map(esc, expr.args[2 : end])
quote
$f($([:(threadlocal($arg)) for arg in args]...))
end
end
so that you can use it as
julia> t = ThreadCache(1)
ThreadCache{Int64}([1])
julia> @threadlocal t + 2
3
That works, but almost defeats the purpose of having this. I basically did it to avoid having to do reduction_storage[threadid()] ... inside loops 24/7. I hoped that multiple dispatch could be really nice here but it seems hard.
Using methodswith in a generated constructor isn’t really a solution either though. It would use non-constant global state (the method table) in the function generator, which is a no-no. At the very least, the behavior of the program could depend on the order in which packages are loaded, as packages may add additional methods and there’s no guarantee on when the generator is run. methodswith would also be a very inaccurate way of finding methods that should be overloaded, since it doesn’t include applicable methods overloaded for a supertype of T, just those overloaded for exactly T.
Perhaps a more advanced macro could walk through a compound (multi-line) expression, find all the function calls (e.g. using MacroTools), and apply the transformation I suggested. That way you wouldn’t have to annotate each line separately. Cassette could also be used to implement (a more advanced version of) this.
Right, changing @threads to a different macro would be a reasonable solution. I will have a go at that with either macro tools or cassette, I’ve been wanting to have a look into cassette anyway !