If the two syntaxes result in exactly the same generated code, should you prefer one over the other? YES. Functions are vastly superior to macros in situations like this.
- Macros are powerful, but they're tricky. You have three errors in your
@IS_COND definition (you don't want to put a type annotation on the argument, you need to interpolate flags into the returned expression, and you need to use esc to get the hygiene correct).
- The function definition just works as you expect.
- Perhaps more importantly, the function works just as others would expect. Macros can do anything, so that
@ sigil is a good warning for "something beyond normal Julia syntax is occurring here." If it's behaving just like a function, though, might as well make it one.
- Functions are first-class objects in Julia; you can pass them around and use them with higher order functions like
map.
- Julia is built on inlined functions. Its performance depends on it! Small functions typically don't even need the
@inline annotation — it just does it on its own. You can use @inline to give the compiler an extra nudge that a bigger function is especially important to inline… but often Julia is good at figuring it out on its own (like here).
- Backtraces and debugging works better with inlined functions than macros.
So, now, do they result in the same generated code? One of the most powerful things about Julia is your ability to ask it for its "intermediate work."
First, some set up:
julia> const COND = UInt(1<<7)
is_cond(flags) = return flags & COND != 0
macro IS_COND(flags)
return :($(esc(flags)) & COND != 0) # careful!
end
Now we can start looking at what happens when you use either is_cond or @IS_COND. In actual code, you'll be using these definitions within other functions, so let's create some test functions:
julia> test_func(x) = is_cond(x)
test_macro(x) = @IS_COND(x)
Now we can start moving down the chain to see if there's a difference. The first step is "lowering" — this simply converts the syntax to a limited subset to make life easier for the compiler. You can see that at this stage, the macro gets expanded but the function call still remains:
julia> @code_lowered test_func(UInt(1))
LambdaInfo template for test_func(x) at REPL[2]:1
:(begin
nothing
return (Main.is_cond)(x)
end)
julia> @code_lowered test_macro(UInt(1))
LambdaInfo template for test_macro(x) at REPL[2]:2
:(begin
nothing
return x & Main.COND != 0
end)
The next step, though, is inference and optimization. It's here that function inlining takes effect:
julia> @code_typed test_func(UInt(1))
LambdaInfo for test_func(::UInt64)
:(begin
return (Base.box)(Base.Bool,(Base.not_int)((Base.box)(Base.Bool,(Base.and_int)((Base.sle_int)(0,0)::Bool,((Base.box)(UInt64,(Base.and_int)(x,Main.COND)) === (Base.box)(UInt64,0))::Bool))))
end::Bool)
julia> @code_typed test_macro(UInt(1))
LambdaInfo for test_macro(::UInt64)
:(begin
return (Base.box)(Base.Bool,(Base.not_int)((Base.box)(Base.Bool,(Base.and_int)((Base.sle_int)(0,0)::Bool,((Base.box)(UInt64,(Base.and_int)(x,Main.COND)) === (Base.box)(UInt64,0))::Bool))))
end::Bool)
Look at that! This step in the internal representation is a little messier, but you can see that the function got inlined (even without @inline!) and now the code looks exactly identical between the two.
We can go farther and ask for the LLVM… and indeed the two are exactly identical:
julia> @code_llvm test_func(UInt(1)) | julia> @code_llvm test_macro(UInt(1))
|
define i8 @julia_test_func_70754(i64) #0 { | define i8 @julia_test_macro_70752(i64) #0 {
top: | top:
%1 = lshr i64 %0, 7 | %1 = lshr i64 %0, 7
%2 = xor i64 %1, 1 | %2 = xor i64 %1, 1
%3 = trunc i64 %2 to i8 | %3 = trunc i64 %2 to i8
%4 = and i8 %3, 1 | %4 = and i8 %3, 1
%5 = xor i8 %4, 1 | %5 = xor i8 %4, 1
ret i8 %5 | ret i8 %5
} | }