Macro to choose macros

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?