It seems I still don’t understand how macros really work in Julia. (sigh)
I want to define a macro @always_fast
which will insert @simd
or Threads.@threads
depending on the value of Threads.nthreads()
for cases like:
@always_fast for i in eachindex(x,y)
x[i] = f(y[i])
end
or
s = 0
@always_fast for y_i in y
s += f(y_i)
end
Macros are just code rewriters. So the first thing to do is to decide what code you would write by hand, without the macro. For example, maybe you want:
@always_fast foo
to turn into
if Threads.nthreads() > 1
Threads.@threads foo
else
@simd foo
end
which could be accomplished with:
macro always_fast(expr)
quote
if Threads.nthreads() > 1
Threads.@threads $(esc(expr))
else
@simd $(esc(expr))
end
end
end
2 Likes
Thanks. Unfortunately
macro always_fast(expr)
quote
if Threads.nthreads() > 1
@inbounds @batch per=thread $(esc(expr))
else
@inbounds @simd $(esc(expr))
end
end
end
y = rand(100,100)
x = similar(y)
@always_fast for I ∈ eachindex(x,y)
x[I] = sin(y[I])
end
gives the error
ERROR: LoadError: ArgumentError: @threads requires a
for loop expression
This is the problem I was having. Both @simd
and Threads.@threads
expect a for loop so this syntax for the macro doesn’t work.
I’m not great at macros myself, so take my reply with a grain of salt, but I had a similar problem. I think the source is that some macros don’t handle escaped expressions. I think it was @turbo
where this happened to me. I ended up doing something analogous to
macro always_fast(expr)
esc(quote
if Threads.nthreads() > 1
@inbounds @batch per=thread $(expr)
else
@inbounds @simd $(expr)
end
end)
end
I think that’s OK for this, but in theory for more complicated code you would presumably have to worry about variable capture. So it doesn’t seem like a very good solution in general. I would also be interested if there’s some better approach.
2 Likes
Right, the esc
was the problem. Putting the esc
outside the quote
should be fine here since your macro doesn’t introduce any new variables that you would have to hygienize.
Otherwise, you could check Meta.isexpr(expr, :for)
and then (for for
loops) create a new for
loop expression where you escape the arguments/body.
2 Likes
Note that Threads.@threads
operates by converting the loop body into a function. To satisfy Julia’s closure semantics, the capture s
is boxed in a mutable container on the heap. This container is type-unstable as per issue #15276, causing runtime dispatch. So, your always-fast loop may not always be fast.
Consider this instead:
s = Threads.Atomic{Int}(0)
@always_fast for y_i in y
Threads.atomic_add!(s, y_i)
end
println(s[])
Good point! Perhaps it would be best to outsource reductions. I see some old questions about this on discourse, but nothing recent and no consensus. Is there a solid multi-threaded reduce operator in a Julia package?