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

2024-03-21

在使用 julia 时,我想要有一个类似于 python 的功能dis模块。 通过网络,我发现 Julia 社区已经解决了这个问题并给出了这些(https://github.com/JuliaLang/julia/issues/218 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

我亲自使用 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        
>>>

任何使用过这些工具的人都可以帮助我更好地理解它们吗?谢谢。


Python 的标准 CPython 实现会解析源代码,并对其进行一些预处理和简化——又名“降低”——将其转换为机器友好、易于解释的格式,称为“bytecode https://en.wikipedia.org/wiki/Bytecode“。这是当您“反汇编”Python 函数时显示的内容。该代码不能由硬件执行 - 它可以由 CPython 解释器“执行”。CPython 的字节码格式相当简单,部分原因是解释器倾向于这样做很好——如果字节码太复杂,它会减慢解释器的速度——部分原因是Python社区倾向于高度重视简单性,有时会以高性能为代价。

Julia 的实现不是解释性的,而是即时 (JIT) 编译 https://en.wikipedia.org/wiki/Just-in-time_compilation。这意味着当您调用函数时,它会转换为由本机硬件直接执行的机器代码。这个过程比 Python 所做的解析和转换为字节码要复杂得多,但作为这种复杂性的交换,Julia 获得了其标志性的速度。 (Python 的 PyPy JIT 也比 CPython 复杂得多,但速度通常也快得多 - 复杂性的增加是相当典型的速度成本。)Julia 代码的四个级别的“反汇编”使您可以访问 Julia 方法的表示从源代码到机器代码转换的不同阶段的特定参数类型的实现。我将使用以下函数来计算其参数后的下一个斐波那契数作为示例:

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

降低代码。 The @code_lowered宏以最接近 Python 字节码的格式显示代码,但它不是供解释器执行,而是供编译器进一步转换。这种格式主要是内部格式,不适合人类使用。代码转换为“单一静态赋值 https://en.wikipedia.org/wiki/Static_single_assignment_form” 形式,其中“每个变量只被分配一次,并且每个变量在使用之前都被定义”。循环和条件语句使用单个变量转换为 goto 和标签unless/goto构造(这不会在用户级 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))

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

键入代码。 The @code_typed宏提供了一组特定参数类型的方法实现类型推断 https://stackoverflow.com/questions/28078089/is-julia-dynamically-typed/28096079#28096079 and inlining https://en.wikipedia.org/wiki/Inline_expansion。代码的这种形式与降低的形式类似,但表达式用类型信息注释,并且一些通用函数调用替换为它们的实现。例如,以下是我们示例函数的类型代码:

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

致电one(n)已被替换为字面意思Int64 value 1(在我的系统上默认整数类型是Int64)。表达方式b < n已被其实施所取代slt_int 固有的 https://en.wikipedia.org/wiki/Intrinsic_function(“有符号整数小于”)并且其结果已用返回类型注释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

这是键入的版本nextfib函数为Int128争论。字面意思1必须符号扩展为Int128操作的结果类型为Int128代替Int64。如果类型的实现有很大不同,则类型化代码可能会有很大不同。例如nextfib for BigInts比简单的“位类型”涉及更多,例如Int64 and Int128:

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

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

LLVM IR.朱莉娅使用LLVM编译器框架 http://llvm.org/生成机器代码。 LLVM 定义了一种类似汇编的语言,它用作共享中间表示 https://en.wikipedia.org/wiki/Intermediate_representation(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
}

这是内存中 LLVM IR 的文本形式nextfib(123)方法实施。 LLVM 并不容易阅读——大多数时候它并不是为了让人们编写或阅读而设计的——但它是彻底的指定并记录 http://llvm.org/docs/LangRef.html。一旦掌握了它的窍门,就不难理解了。这段代码跳转到标签处L4并初始化“寄存器”%storemerge1 and %storemergei64(LLVM 的名称为Int64) value 1(当从不同的位置跳转到时,它们的值会得到不同的结果 - 这就是phi指令确实)。然后它会执行一个icmp slt比较%storemerge带寄存器%0– 在整个方法执行过程中保持参数不变 – 并将比较结果保存到寄存器中%1。它做了一个add i64 on %storemerge and %storemerge1并将结果保存到寄存器中%2. If %1是真的,它分支回到L4否则它分支到L13。当代码循环回到L4登记册%storemerge1获取之前的值%storemerge and %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)

这是在 Intel Core i7 上,属于 x86_64 CPU 系列。它仅使用标准整数指令,因此架构是什么并不重要,但是根据特定的架构,某些代码可能会得到不同的结果your机,因为 JIT 代码在不同的系统上可能不同。这pushq and movq开头的指令是标准函数前导码,将寄存器保存到堆栈中;相似地,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

The 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/ 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 http://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-2-an-x64-jit.html
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Julia 中的 @code_native、@code_typed 和 @code_llvm 有什么区别? 的相关文章

  • 如何在命令行中执行 Julia 代码?

    我最近在 Julia 中转移了我的代码 我想知道如何在命令行中执行 Julia 代码 我知道 Julia 代码可以通过运行一次来 编译 但问题是我需要对集 群上的模拟模型进行参数扫描 我只能使用命令行 而不能使用 REPL 在集群上运行模拟
  • Julia 自定义类型分配

    我尝试从 Julia 中的自定义类型分配多个元素 但是我不知道该怎么做 或者换句话说 我想重载赋值运算符以返回该类型中包含的所有元素的元组 这是所需的行为 type foo a b end a b foo 1 2 a gt 1 这是错误消息
  • 在 Julia 中迭代具有不同数量参数的不同函数

    我正在尝试使用不同数量的参数对不同的函数运行循环 变量是在运行时在循环内创建的 我想在每次迭代时使用 eval 来使用变量 symbol 实例化一个 Struct 但是 我不能这样做 因为 eval 只在全局范围内有效 这是有效案例的 MW
  • Julia:生成唯一的随机整数数组

    我正在尝试创建 10 个唯一随机整数的元素数组 但是我无法创建具有唯一值的数组 Julia 中是否有类似 Python 的东西样本函数 https docs python org 2 library random html random s
  • 如何在 Julia 中提供可重现的样本数据

    Here on stackoverflow com 当我提供样本数据来制作可重现的示例时 我该如何以朱利安方式做到这一点 In R例如dput df 将输出一个字符串 您可以用它来创建df再次 因此 您只需将结果发布到 stackoverf
  • 如何创建并推送到共享或分布式数组数组?

    我编写了 Julia 代码 其中初始化一个空数组 如下所示 a 稍后在代码中 我简单地推送到该数组 如下所示 推 a b 其中 b c d e 是另一个数组 每个 b 可以具有不同的长度 这在非并行化代码中工作得很好 但是 我想在并行代码中
  • Julia:显示函数体(以查找丢失的代码)

    在 R 语言中 我可以声明一个函数并查看函数体 如下所示 gt megafoobar function x return x 10000 gt body megafoobar return x 10000 类似的事情在 Julia 中也可能
  • 在 Julia 中,有没有办法让“现在”(至少)达到毫秒精度?

    通常 要了解代码中发生的情况 您需要高精度时间来分析您的应用程序或出于其他原因 显然 现在 https stackoverflow com questions 32407509 how to get the milliseconds fro
  • @distributed 似乎有效,函数返回很不稳定

    我正在学习如何在 Julia 中进行并行计算 我在用着 sync distributed在 3x 嵌套的开始处for循环并行化事物 参见底部的代码 从线路上看println errCmp row col 我可以观察数组的所有元素errCmp
  • 使用 Julia 的 Debugger.jl - 如何进入类似于 Python 的 pdb.set_trace() 或 ipdb.set_trace() 的调试模式?

    Julia 的新 Debugger jl 很棒 但有时要达到我想要达到的代码中的确切位置有点痛苦 有没有办法可以进入交互式调试模式 类似于 Python 在 pdb set trace 或 ipdb set trace 中的模式 例如 我希
  • 用以前的非缺失值填充“缺失”值的有效方法是什么?

    我有一个向量 using Missings v allowmissing rand 100 v rand 100 lt 0 1 missing 最好的填充方式是什么v与最后一个非缺失值 现在 for i val in enumerate v
  • 如何给DArray的元素设置值?

    我正在探索 Julia 的并行计算并尝试了以下方法 a dzeros 5 a 1 5 但刚刚收到此错误 setindex not defined for DArray Float64 1 Array Float64 1 嗯 我以为手册上说s
  • 为什么 Julia 中的“where”语法对换行符敏感?

    在 Stack Overflow 上的另一个问题中 答案包括以下函数 julia gt function nzcols b SubArray T 2 P Tuple UnitRange Int64 UnitRange Int64 where
  • 如何在 Julia 中保存文件

    在某些时候 我认为 Julia v0 7 你可以做 save savepath thingtosave为了使用 Julia 保存文件 我尝试在 v0 7 上运行它 看看是否收到弃用警告 但即使在 0 7 上 它也说 save未定义 如何使用
  • Julia:将数组数组转换为二维数组

    我有一个数组d包含一个浮点数组 julia gt d 99 element Array Array Float64 1 1 我正在尝试将其转换为二维数组 并且我成功地实现了我的目标 data Array Float64 length d l
  • Julia:如何更新到软件包的最新版本(即 Flux)

    I have Julia 1 1 在本例中 我想更新到软件包的最新版本Flux 8 3 0根据Flux jl 的文档 https fluxml ai Flux jl stable 当我打字时 Pkg status Flux I get St
  • 在我的 Julia 1.0.0 REPL 中,LOAD_PATH 返回意外结果

    我的 Julia REPL 帮助为 LOAD PATH 提供了以下内容 help gt LOAD PATH search LOAD PATH LOAD PATH An array of paths for using and import
  • 如何在 Julia 中转置字符串数组?

    它适用于数字 但不适用于字符串 The 1 2 有效 但是 a b 没有 为什么 以及如何做到这一点 Why a b 不起作用 因为 运算符实际上计算矩阵的 惰性 伴随 请注意 如文档中所述 adjoint https docs julia
  • 如何在 Julia 中有效计算二次形式?

    我想计算一个二次形式 x Q y在朱莉娅 对于这种情况 计算此值的最有效方法是什么 没有假设 Q是对称的 x and y是相同的 x y Both Q是对称的并且x y 我知道朱莉娅有dot 但我想知道它是否比 BLAS 调用更快 现有的答
  • 从 Julia 更新 C 结构体的字段值

    我的问题很简单 但我不知道最好的方法 或者 Julia 目前没有提供这样的方法 如何从 Julia 设置 C 结构的字段值 假设您有一个结构类型来表示 C 库中树的节点 typedef struct node s int type node

随机推荐