Julia中的@ code_native,@ code_typed和@code_llvm有什么区别?

Rah*_*hul 22 julia

在浏览julia时,我想要一个类似于python dis模块的功能.通过网络,我发现Julia社区已经解决了这个问题并给出了这些(https://github.com/JuliaLang/julia/issues/218)

finfer -> code_typed
methods(function, types) -> code_lowered
disassemble(function, types, true) -> code_native
disassemble(function, types, false) -> code_llvm
Run Code Online (Sandbox Code Playgroud)

我亲自使用Julia REPL尝试了这些,但我似乎很难理解.

在Python中,我可以反汇编这样的函数.

>>> import dis
>>> dis.dis(lambda x: 2*x)
  1           0 LOAD_CONST               1 (2)
              3 LOAD_FAST                0 (x)
              6 BINARY_MULTIPLY     
              7 RETURN_VALUE        
>>>
Run Code Online (Sandbox Code Playgroud)

任何使用过这些东西的人都可以帮助我更多地理解它们吗?谢谢.

Ste*_*ski 47

Python的标准CPython实现解析源代码并对其进行一些预处理和简化 - 也称为"降低" - 将其转换为机器友好,易于理解的格式,称为" 字节码 ".这是"反汇编"Python函数时显示的内容.此代码不可由硬件执行 - 它由CPython解释器"可执行".CPython的字节码格式相当简单,部分原因是因为这是解释器倾向于做得很好的 - 如果字节码太复杂,它会降低解释器的速度 - 部分原因是因为Python社区倾向于高度重视简单性,有时需要付出代价高性能.

Julia的实现没有被解释,它是即时(JIT)编译的.这意味着当您调用函数时,它将转换为由本机硬件直接执行的机器代码.这个过程比解析和降低到Python所做的字节码要复杂得多,但为了换取这种复杂性,Julia得到了它的标志性速度.(用于Python的PyPy JIT也比CPython复杂得多,但通常也要快得多 - 增加的复杂性是速度的一个相当典型的成本.)Julia代码的四个"反汇编"级别使您可以访问Julia方法的表示在从源代码到机器代码的转换的不同阶段的特定参数类型的实现.我将使用以下函数来计算其参数后面的下一个Fibonacci数作为示例:

function nextfib(n)
    a, b = one(n), one(n)
    while b < n
        a, b = b, a + b
    end
    return b
end

julia> nextfib(5)
5

julia> nextfib(6)
8

julia> nextfib(123)
144
Run Code Online (Sandbox Code Playgroud)

降低代码.所述@code_lowered的格式是最接近的Python字节代码,但而不是由解释用于执行宏显示代码,它用于由编译器进一步转化.这种格式主要是内部的,不适合人类消费.代码转换为" 单一静态分配 "形式,其中"每个变量只分配一次,每个变量在使用前定义".使用单个unless/ goto构造将循环和条件转换为gotos和标签(这不在用户级Julia中公开).这是我们降低形式的示例代码(在Julia 0.6.0-pre.beta.134中,这正是我碰巧可用的):

julia> @code_lowered nextfib(123)
CodeInfo(:(begin
        nothing
        SSAValue(0) = (Main.one)(n)
        SSAValue(1) = (Main.one)(n)
        a = SSAValue(0)
        b = SSAValue(1) # line 3:
        7:
        unless b < n goto 16 # line 4:
        SSAValue(2) = b
        SSAValue(3) = a + b
        a = SSAValue(2)
        b = SSAValue(3)
        14:
        goto 7
        16:  # line 6:
        return b
    end))
Run Code Online (Sandbox Code Playgroud)

您可以看到SSAValue节点和unless/ goto构造以及标签号.这并不难读,但同样,它也不是真正意味着人类消费的容易程度.降低代码不依赖于参数的类型,除非它们确定要调用哪个方法体 - 只要调用相同的方法,相同的降低的代码就适用.

键入的代码.@code_typed宏给出了后一组特定的参数类型的方法实现类型推断内联.代码的这种化身类似于降低的形式,但表达式用类型信息注释,一些泛型函数调用用它们的实现替换.例如,这是我们的示例函数的类型代码:

julia> @code_typed nextfib(123)
CodeInfo(:(begin
        a = 1
        b = 1 # line 3:
        4:
        unless (Base.slt_int)(b, n)::Bool goto 13 # line 4:
        SSAValue(2) = b
        SSAValue(3) = (Base.add_int)(a, b)::Int64
        a = SSAValue(2)
        b = SSAValue(3)
        11:
        goto 4
        13:  # line 6:
        return b
    end))=>Int64
Run Code Online (Sandbox Code Playgroud)

调用one(n)已被文字Int64值替换1(在我的系统上是默认的整数类型Int64).该表达式b < n已根据其slt_int 内部("有符号整数小于")替换为其实现,并且其结果已使用返回类型进行注释Bool.该表达式a + b也被替换为其add_int内在及其结果类型注释为的实现Int64.并且整个函数体的返回类型已注释为Int64.

与降低代码(仅依赖于参数类型来确定调用哪个方法体)不同,类型化代码的细节取决于参数类型:

julia> @code_typed nextfib(Int128(123))
CodeInfo(:(begin
        SSAValue(0) = (Base.sext_int)(Int128, 1)::Int128
        SSAValue(1) = (Base.sext_int)(Int128, 1)::Int128
        a = SSAValue(0)
        b = SSAValue(1) # line 3:
        6:
        unless (Base.slt_int)(b, n)::Bool goto 15 # line 4:
        SSAValue(2) = b
        SSAValue(3) = (Base.add_int)(a, b)::Int128
        a = SSAValue(2)
        b = SSAValue(3)
        13:
        goto 6
        15:  # line 6:
        return b
    end))=>Int128
Run Code Online (Sandbox Code Playgroud)

这是参数nextfib函数的类型化版本Int128.文字1必须符号扩展为Int128,操作的结果类型是类型Int128而不是Int64.如果类型的实现有很大不同,则类型化代码可能完全不同.例如nextfib对于BigInts是显著比简单更多地参与"位类型"喜欢Int64Int128:

julia> @code_typed nextfib(big(123))
CodeInfo(:(begin
        $(Expr(:inbounds, false))
        # meta: location number.jl one 164
        # meta: location number.jl one 163
        # meta: location gmp.jl convert 111
        z@_5 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 112:
        $(Expr(:foreigncall, (:__gmpz_set_si, :libgmp), Void, svec(Ptr{BigInt}, Int64), :(&z@_5), :(z@_5), 1, 0))
        # meta: pop location
        # meta: pop location
        # meta: pop location
        $(Expr(:inbounds, :pop))
        $(Expr(:inbounds, false))
        # meta: location number.jl one 164
        # meta: location number.jl one 163
        # meta: location gmp.jl convert 111
        z@_6 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 112:
        $(Expr(:foreigncall, (:__gmpz_set_si, :libgmp), Void, svec(Ptr{BigInt}, Int64), :(&z@_6), :(z@_6), 1, 0))
        # meta: pop location
        # meta: pop location
        # meta: pop location
        $(Expr(:inbounds, :pop))
        a = z@_5
        b = z@_6 # line 3:
        26:
        $(Expr(:inbounds, false))
        # meta: location gmp.jl < 516
        SSAValue(10) = $(Expr(:foreigncall, (:__gmpz_cmp, :libgmp), Int32, svec(Ptr{BigInt}, Ptr{BigInt}), :(&b), :(b), :(&n), :(n)))
        # meta: pop location
        $(Expr(:inbounds, :pop))
        unless (Base.slt_int)((Base.sext_int)(Int64, SSAValue(10))::Int64, 0)::Bool goto 46 # line 4:
        SSAValue(2) = b
        $(Expr(:inbounds, false))
        # meta: location gmp.jl + 258
        z@_7 = $(Expr(:invoke, MethodInstance for BigInt(), :(Base.GMP.BigInt))) # line 259:
        $(Expr(:foreigncall, ("__gmpz_add", :libgmp), Void, svec(Ptr{BigInt}, Ptr{BigInt}, Ptr{BigInt}), :(&z@_7), :(z@_7), :(&a), :(a), :(&b), :(b)))
        # meta: pop location
        $(Expr(:inbounds, :pop))
        a = SSAValue(2)
        b = z@_7
        44:
        goto 26
        46:  # line 6:
        return b
    end))=>BigInt
Run Code Online (Sandbox Code Playgroud)

这反映了这样一个事实,即操作BigInts非常复杂,涉及内存分配和外部GMP库调用(libgmp).

LLVM IR.Julia使用LLVM编译器框架生成机器代码.LLVM定义了一种类似于汇编的语言,它用作不同编译器优化过程和框架中其他工具之间的共享中间表示(IR).LLVM IR有三种同构形式:

  1. 紧凑且机器可读的二进制表示.
  2. 一种冗长且有些人类可读的文本表示.
  3. LLVM库生成和使用的内存中表示形式.

Julia使用LLVM的C++ API在内存中构建LLVM IR(表单3),然后在该表单上调用一些LLVM优化传递.当您这样做时,@code_llvm您会看到LLVM IR生成后的一些高级优化.这是我们正在进行的示例的LLVM代码:

julia> @code_llvm nextfib(123)

define i64 @julia_nextfib_60009(i64) #0 !dbg !5 {
top:
  br label %L4

L4:                                               ; preds = %L4, %top
  %storemerge1 = phi i64 [ 1, %top ], [ %storemerge, %L4 ]
  %storemerge = phi i64 [ 1, %top ], [ %2, %L4 ]
  %1 = icmp slt i64 %storemerge, %0
  %2 = add i64 %storemerge, %storemerge1
  br i1 %1, label %L4, label %L13

L13:                                              ; preds = %L4
  ret i64 %storemerge
}
Run Code Online (Sandbox Code Playgroud)

这是nextfib(123)方法实现的内存LLVM IR的文本形式.LLVM不容易阅读 - 大多数时候它并不打算由人们编写或阅读 - 但它已经过彻底的指定和记录.一旦掌握了它,就不难理解了.此代码跳转到标签L4并初始化"寄存器" %storemerge1%storemerge使用i64(LLVM的名称Int64)值1(当从不同位置跳转时,它们的值的导出方式不同 - 这就是phi指令的作用).然后它与寄存器icmp slt进行比较- 它保持整个方法执行不受影响的参数 - 并将比较结果保存到寄存器中.它打开并将结果保存到寄存器中.如果是真的,它会分支回来,否则它会分支到.当代码循环回寄存器时,获取先前的值并得到之前的值.%storemerge%0%1add i64%storemerge%storemerge1%2%1L4L13L4%storemerge1%storemerge%storemerge%2

本机代码.由于Julia执行本机代码,因此方法实现所采用的最后一种形式是机器实际执行的内容.这只是内存中的二进制代码,相当难以阅读,所以很久以前人们发明了各种形式的"汇编语言",它们代表指令和寄存器的名称,并有一些简单的语法来帮助表达指令的作用.通常,汇编语言与机器代码保持一致的对应关系,特别是,人们总是可以将机器代码"反汇编"为汇编代码.这是我们的例子:

julia> @code_native nextfib(123)
    .section    __TEXT,__text,regular,pure_instructions
Filename: REPL[1]
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $1, %ecx
    movl    $1, %edx
    nop
L16:
    movq    %rdx, %rax
Source line: 4
    movq    %rcx, %rdx
    addq    %rax, %rdx
    movq    %rax, %rcx
Source line: 3
    cmpq    %rdi, %rax
    jl  L16
Source line: 6
    popq    %rbp
    retq
    nopw    %cs:(%rax,%rax)
Run Code Online (Sandbox Code Playgroud)

这是在Intel Core i7上,它位于x86_64 CPU系列中.它只使用标准整数指令,因此除了架构之外没有关系,但是根据机器的特定架构,可以为某些代码获得不同的结果,因为JIT代码在不同系统上可能不同.的pushqmovq在开始指令是一个标准的功能前导码,保存寄存器到堆栈; 同样,popq恢复寄存器并retq从函数返回; nopw是一个2字节的指令,什么都不做,只包括填充函数的长度.所以代码的核心就是:

    movl    $1, %ecx
    movl    $1, %edx
    nop
L16:
    movq    %rdx, %rax
Source line: 4
    movq    %rcx, %rdx
    addq    %rax, %rdx
    movq    %rax, %rcx
Source line: 3
    cmpq    %rdi, %rax
    jl  L16
Run Code Online (Sandbox Code Playgroud)

movl顶部的指令初始化寄存器有1个值.的movq指令的寄存器间移动值和addq指令将寄存器.该cmpq指令比较两个寄存器,并jl跳转回L16或继续从该函数返回.紧凑循环中的一些整数机器指令正是在运行Julia函数调用时执行的,以稍微更令人愉悦的人类可读形式呈现.很容易看出为什么它运行得很快.

如果你对JIT编译感兴趣,那么与解释的实现相比,Eli Bendersky有一对很棒的博客文章,他从一个简单的语言解释器实现到同一语言的(简单)优化JIT:

  1. http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-1-an-interpreter/
  2. http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-2-an-x64-jit.html