朱莉娅-@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更快,这对我来说并不明显。暂时忽略这种情况下的性能差异可以忽略不计,哪种技术可以产生最有效的代码?

马特·B。

如果两种语法生成的代码完全相同,那么您是否应该优先使用另一种?是的在这种情况下,函数在很大程度上优于宏。

  • 宏功能强大,但是很棘手。定义中存在三个错误@IS_COND(您不想在参数上添加类型注释,您需要对flags返回的表达式进行插值,并且需要使用它esc来确保正确无误。)
  • 函数定义正好按您期望的那样工作。
  • 也许更重要的是,该功能可以像其他人期望的那样工作。宏可以做任何事情,因此@对于“在这里发生的常规Julia语法以外的事情” sigil是一个很好的警告。但是,如果它的行为就像一个函数,那么不妨使其成为一个函数。
  • 函数是Julia中的一类对象;您可以将它们传递给更高的函数,例如map
  • Julia是基于内联函数构建的。它的性能取决于它!小型函数通常甚至不需要@inline注解,它只是自己做。您可以@inline用来使编译器更加轻巧,即更大的功能对于内联尤为重要……但是Julia通常擅长自行解决(如此处所示)。
  • 内联函数的回溯和调试效果比宏更好。

那么,现在,它们是否产生相同的生成代码?关于Julia的最强大的功能之一就是您有能力要求它提供“中间工作”。

首先,进行一些设置:

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)

不过,下一步是推理和优化。函数内联在这里生效:

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
}                                          | }

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章