Джулия - как работает @inline? Когда использовать функцию против макроса?

У меня есть много небольших функций, которые я хотел бы встроить, например, для проверки флагов для некоторого условия:

const COND = UInt(1<<BITS_FOR_COND)
function is_cond(flags::UInt)
    return flags & COND != 0
end

Я мог бы сделать макрос:

macro IS_COND(flags::UInt)
    return :(flags & COND != 0)
end

Моя мотивация - это много подобных макрофункций в коде C, с которым я работаю:

#define IS_COND(flags) ((flags) & COND)

Я неоднократно приурочил функцию, макрос, функцию, определенную с помощью @inline, и само выражение, но ни один из них не был последовательно быстрее других, чем во многих прогонах. Сгенерированный код для вызова функции в 1) и 3) намного длиннее, чем для выражения в 4), но я не знаю, как сравнивать 2), так как @code_llvm и т.д. Не работают с другими макросами.

1) for j=1:10 @time for i::UInt=1:10000 is_cond(i); end end
2) for j=1:10 @time for i::UInt=1:10000 @IS_COND(i); end end
3) for j=1:10 @time for i::UInt=1:10000 is_cond_inlined(i); end end
4) for j=1:10 @time for i::UInt=1:10000 i & COND != 0; end end

Вопросы: Какова цель @inline? Из редкой документации я вижу, что она добавляет символ :inline к выражению :meta, но что именно это делает? Есть ли причина предпочесть функцию или макрос для этой задачи?

Мое понимание заключается в том, что функция макроса C просто заменяет буквальный текст макроса во время компиляции, поэтому полученный код не имеет переходов и, следовательно, более эффективен, чем обычный вызов функции. (Безопасность - еще одна проблема, но пусть программисты знают, что они делают.) Макрос Julia имеет промежуточные шаги, такие как анализ его аргументов, поэтому для меня не очевидно, будет ли 2) быть быстрее, чем 1). Игнорируя на данный момент, что в этом случае разница в производительности пренебрежимо мала, какой метод дает самый эффективный код?

Ответ 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
}                                          | }