Wrapper types function passthrough

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.

Thanks!

1 Like

If I understand you correctly, you would like to have that

f(t::ThreadCache, args...; kw...) = f(cache(t), args...; kw...)

for any function f automatically. This is not possible in julia. You have to list the functions f you want to support explicitly.

1 Like

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.

Yeah, this is basically https://github.com/JuliaLang/julia/issues/14919

julia> (f::Any)(x::ThreadCache, args...; kwargs...) = 1
ERROR: cannot add methods to an abstract type
Stacktrace:
 [1] top-level scope at none:0

You can definitely work around this with a pretty simple macro though.

1 Like

Would the macro be something along the lines of an @generated in front of the constructor that also evals all methodswith of T?

Edit: I had a quick look through the issue and there might be some other suggestions I could try.

No, I was thinking more along the lines of this:

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
2 Likes

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.

Yeah, it’s unfortunate.

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 !