Ответ 1
Если два синтаксиса приводят к точному сгенерированному коду, вы должны предпочесть один из них? Да. Функции значительно превосходят макросы в таких ситуациях.
- Макросы мощные, но они сложны. У вас есть три ошибки в определении
@IS_COND
(вы не хотите помещать аннотацию типа в аргумент, вам нужно интерполироватьflags
в возвращаемое выражение, и вам нужно использоватьesc
для правильной гигиены). - Определение функции работает так, как вы ожидаете.
- Возможно, что более важно, функция работает так же, как ожидали другие. Макросы могут делать что угодно, так что
@
sigil является хорошим предупреждением для "что-то сверх обычного синтаксиса Julia происходит здесь". Однако, если он ведет себя точно так же, как функция, он может сделать его одним. - Функции являются первоклассными объектами в Julia; вы можете передать их и использовать их с более высокими функциями порядка, например
map
. - Julia построена на встроенных функциях. Его производительность зависит от этого! Маленьким функциям обычно даже не нужна аннотация
@inline
- она просто делает это сама по себе. Вы можете использовать@inline
, чтобы дать компилятору дополнительное подталкивание, что большая функция особенно важна для встроенных... но часто Джулия хорошо разбирается в этом самостоятельно (например, здесь). - Backtraces и debugging работают лучше с встроенными функциями, чем макросами.
Итак, теперь они приводят к тому же сгенерированному коду? Одна из самых сильных вещей о Джулии - это ваша способность просить ее о "промежуточной работе".
Во-первых, некоторые настроены:
julia> const COND = UInt(1<<7)
is_cond(flags) = return flags & COND != 0
macro IS_COND(flags)
return :($(esc(flags)) & COND != 0) # careful!
end
Теперь мы можем начать смотреть на то, что происходит, когда вы используете либо is_cond
, либо @IS_COND
. В фактическом коде вы будете использовать эти определения в других функциях, поэтому создайте некоторые тестовые функции:
julia> test_func(x) = is_cond(x)
test_macro(x) = @IS_COND(x)
Теперь мы можем начать движение по цепочке, чтобы увидеть, есть ли разница. Первый шаг - "понижение" - это просто преобразует синтаксис в ограниченное подмножество, чтобы облегчить жизнь компилятору. Вы можете видеть, что на этом этапе макрос расширяется, но вызов функции по-прежнему остается:
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)
Следующим шагом, однако, является вывод и оптимизация. Именно здесь вступает в действие функция inline:
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)
Посмотрите на это! Этот шаг во внутреннем представлении немного грязнее, но вы можете видеть, что функция получила встроенный (даже без @inline
!), И теперь код выглядит точно идентичным между ними.
Мы можем идти дальше и просить LLVM... и действительно, оба они абсолютно идентичны:
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
} | }