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 有什么区别? 的相关文章

  • R / Python / Julia 中 Matlab 的类型转换函数相当于什么

    相当于Matlab的什么typecastR 中的函数 在Python中 在朱莉娅 Matlab的typecast函数的描述如下 typecast http www mathworks com help matlab ref typecast
  • 处理 Julia 中 kwargs 的类型

    我该如何使用kwargs在 Julia 函数中并声明其类型以提高速度 function f x Float64 kwargs kwargs Dict kwargs if haskey kwargs c c Float64 kwargs c
  • Julia 中的内存分配

    将程序从 Python 翻译成 Julia 后 我非常不满意 对于小 非常小的输入 Python 更快 对于中等输入 Julia 更快 但没那么快 对于大输入 Python 更快 我认为原因是我不明白内存分配是如何工作的 这里自学者 没有C
  • 类型稳定性如何让 Julia 如此之快?

    我听说类型稳定性使 Julia 如此之快 同时仍然与其他解释语言 例如 Python 一样具有表达能力 类型稳定性允许编译器在编译时直接根据输入类型确定函数的输出类型 因为 Julia 专门针对每种输入类型进行编译 这意味着如果所有函数都是
  • Julia - 迭代字典中的键组合

    有没有一种巧妙的方法来迭代字典中的键组合 我的字典有这样的值 1 gt 1 2 2 3 gt 15 3 gt 6 7 8 4 9 11 gt 3 我需要做的是获取所有长度的键组合1 n where n可能是FX 3 就像上面的例子一样 我想
  • Julia:数组是否包含特定的子数组

    在 julia 中 我们可以检查数组是否包含值 如下所示 gt 6 in 4 6 5 true 然而 当尝试按特定顺序检查子数组时 这会返回 false gt 4 6 in 4 6 5 false 验证数组中是否存在特定子数组的正确语法是什
  • 如何更改julia(1.8.5)默认安装包的位置?

    如何更改julia 1 8 5 默认安装包的位置 我不想把包安装到c盘 因为pwd 和homedir 都默认到c盘 如何修改 我想每次都像安装python包一样打开Julia 有固定的安装路径 例如user Base User site 我
  • Julia 似乎没有使用字符串来执行插值

    官方文档指出 连接和字符串插值调用string 将对象转换为字符串形式 然而 以下最小工作示例似乎证明了其他情况 type MyType x Int end import Base string Base string m MyType w
  • 有没有办法在 writetable() 中使用字符串作为分隔符 - Julia

    当使用 writetable 将数据帧写入文件时 我希望能够将分隔符设为空格然后逗号 即 作为分隔符 我知道 writetable 只能选择将单个字符作为分隔符参数 是否有可能的解决方法能够将字符串作为分隔符 或者 是否可以简单地在数据框中
  • 埃拉托斯特尼筛法速度比较:Python 与 Julia

    所以我有一个用 Python 和 Julia 编写的 Eratosthenes 函数的小筛子 并且我正在比较运行时间 这是Python代码 import time def get primes n numbers set range n 1
  • 在 Julia 中提取参数类型

    假设我在 Julia 中编写了一个函数 它接受Dict K V 作为参数 然后创建类型的数组Array K 1 and Array V 1 我怎样才能提取类型K and V来自 Dict 对象 以便我可以使用它们来创建数组 斯文和约翰的答案
  • 朱莉娅中未显示情节

    我有一个名为 mycode jl 的文件 其中包含以下代码here https juliastats org MultivariateStats jl dev pca using MultivariateStats RDatasets Pl
  • Julia:如何让多个工作人员访问模块中的函数?

    我有以下测试模块 MyMod jl 来在 Julia 中存储一些测试函数 一些核心函数是串行编写的 其他函数并行调用核心函数 module MyMod export Dummy distribute data getfrom recombi
  • Julia 中过时的软件包列表

    有没有办法列出 Julia 中所有过时的软件包 相当于pip3 list outdated在Python中 我做了几次搜索 1 https docs julialang org en v1 stdlib Pkg 2 https pkgdoc
  • 带有 return 语句的 Julia @parallel for 循环

    如何在满足条件时立即返回所有工作人员的函数中编写并行 for 循环 IE 像这样的东西 function test n sync parallel for i in 1 1000 statement if condition return
  • 如何在一张图中显示多个相邻的图像?

    我正在使用 Julia PyPlot 和 Images 来处理一些图片 因为显示几十个图是不可能的 所以我想堆叠图像 如下所示 我有图像数据Array Array Float64 2 1 已经正常化了 以下代码仅显示最后一个最小的图像 该数
  • 估算缺失数据,同时强制相关系数保持不变

    考虑以下 excel 数据集 m r 2 0 3 3 0 8 4 0 1 3 2 1 5 2 2 3 1 9 2 5 1 2 3 0 2 0 2 6 我的目标是使用以下条件填充缺失值 将上述两列之间的成对相关性表示为 R 大约 0 68 将
  • 为什么 Julia 中的“where”语法对换行符敏感?

    在 Stack Overflow 上的另一个问题中 答案包括以下函数 julia gt function nzcols b SubArray T 2 P Tuple UnitRange Int64 UnitRange Int64 where
  • 如何从字符串转换为数组?

    If s 1 2 3 4 5 我们如何从中获得一个整数数组 我想返回 5 个元素Array Int64 1 1 2 3 4 5 正如 isebarn 使用的那样 split s 对于将字符串拆分为单词非常有用 默认情况下按空格拆分 juli
  • 朱莉娅在矩阵中查找(行,列)而不是索引

    在 Julia 中 您可以通过以下方式找到矩阵中元素的坐标 julia gt find x gt x 2 1 2 3 2 3 4 1 0 2 3 element Array Int64 1 2 4 9 这些值是正确的 但我更希望得到 row

随机推荐