贡献者: addis
本文是关于 Julia 解释器的原理:它使用了哪些技术,可以使得它作为一门动态语言能达到编译语言的性能。
LLVM.jl 包可以把 julia 代码直接生成 LLVM IR,或者把 Julia IR 转为 LLVM IR。
Meta.parse 是用户使用时表面上的 parser。从 1.10 开始,它底层替换为 JuliaSyntax,使用 JuliaSyntax.parse! 或者 JuliaSyntax.parsestmt。
Core.Compiler 模块实现的
dump(表达式) 函数可以显示语法树,如 expr = :(x + 2y) 显示
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Symbol x
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Int64 2
3: Symbol y
Expr 类型是内建的,似乎无法查看源码。
dump(类型) 可以显示该类型的定义。dump(Expr) 会打印
Expr <: Any
head::Symbol
args::Vector{Any}
Expr(表达头, 入参1, 入参2, ...) 如 ex = Expr(:call, :+, 1, 2)
Meta.show_sexpr() 可以将 Expr 表示为 lisp 的 S-expression
@which dump(:(x + 2y)) 可以看到该函数在哪里定义(C:\Users\addis\AppData\Local\Programs\Julia-1.11.7\share\julia\base\show.jl)。
dump(arg; maxdepth=最大层数) 可以指定最大层数。
function dump(io::IOContext, @nospecialize(x), n::Int, indent)
$(...) 可以出现在 quote ... end 中,直接把一部分表达式求值并插入而不是把 ... 作为表达式插入。例如 :(a + b) 中 +, a, b 都是 Symbol,但 a = 1; ex = :($(a+1) + b) 得到的就是 :(2 + b)
st =
quote
struct MySt
a::Int64
b::String
end
end
dump(Base.remove_linenums!(st)) 的结果是
Expr
head: Symbol block
args: Array{Any}((1,))
1: Expr
head: Symbol struct
args: Array{Any}((3,))
1: Bool false
2: Symbol MySt
3: Expr
head: Symbol block
args: Array{Any}((2,))
1: Expr
head: Symbol ::
args: Array{Any}((2,))
1: Symbol a
2: Symbol Int64
2: Expr
head: Symbol ::
args: Array{Any}((2,))
1: Symbol b
2: Symbol String
首先安装 using Pkg; Pkg.add("LLVM")(其实没必要)
在 REPL 中输入如下代码
function add(x::Int, y::Int)::Int
return x + y
end
lowered_ir = @code_lowered add(1, 2) # 生成 Lowered Julia IR
typed_ir = @code_typed add(1, 2) # 生成静态类型的 Julia IR
llvm_ir = @code_llvm add(1, 2) # 生成 LLVM IR
也可以把函数定义写到 jl 脚本中,然后在 REPL 中 include("脚本名.jl"),再 @code_xxx add(1, 2)。include() 相当于把脚本代码直接插入到该位置。如果找不到,就用 using InteractiveUtils; a = InteractiveUtils.@code_lowered add(1, 2);
注意只会生成指定函数的 IR,不会递归生成它调用的其他函数的 IR。
julia> lowered_ir = @code_lowered add(1, 2)
CodeInfo(
1 ─ %1 = Main.Int
│ %2 = Main.:+
│ %3 = (%2)(x, y)
│ @_4 = %3
│ %5 = @_4
│ %6 = %5 isa %1
└── goto #3 if not %6
2 ─ goto #4
3 ─ %9 = @_4
│ %10 = Base.convert(%1, %9)
└── @_4 = Core.typeassert(%10, %1)
4 ┄ %12 = @_4
└── return %12
)
生成的 LLVM IR 代码如下
注意返回的 lowered_ir 变量并不是一个字符串,而是结构化的信息。
如果 Julia IR 中的东西不懂,文档也不完善,那就直接执行试试即可,Julia IR 中的函数都是可以在 REPL 中执行的。例如上面的 Main.Int 执行出来就是 Int64::DataType。找许多 Julia 源码和 IR 对照下来基本就能明白。
代码
for n in 1:5
c += n
end
对应的 IR 是
14 ┄ %77 = Main.:(:) ; for n in 1:5
; % ==循环准备阶段==
│ %78 = (%77)(1, 5) ; % 返回 ::UnitRange{Int64}
│ @_4 = Base.iterate(%78) ; % 初始化迭代器: (值, 循环指数)
; % 类型 Tuple{Int64, Int64}
; % , 当前为 (1, 1)
│ %80 = @_4
│ %81 = %80 === nothing
│ %82 = Base.not_int(%81) ; % 非运算
└─── goto #17 if not %82
; % ==循环体==
15 ┄ %84 = @_4
│ n = Core.getfield(%84, 1)
│ %86 = Core.getfield(%84, 2) ; % 循环指数(对 UnitRange{Int64}
; % 未必从 1 开始也未必步长为 1)
│ %87 = Main.:+ ; c += n
│ %88 = c
│ %89 = n
│ c = (%87)(%88, %89)
│ @_4 = Base.iterate(%78, %86) ; % 迭代(nothing 表示结束)
│ %92 = @_4
│ %93 = %92 === nothing
│ %94 = Base.not_int(%93)
└─── goto #17 if not %94
16 ─ goto #15
; end
代码
function foo()
a = 0
for i in [1, "123", 1+2im]
a += 1
println(i, a);
end
end
对应的 IR 是
CodeInfo(
1 ─ a = 0
; % ==循环准备==
│ %2 = Main.:+ ; % 构造 [1, "123", 1+2im]
│ %3 = Main.:*
│ %4 = Main.im
│ %5 = (%3)(2, %4)
│ %6 = (%2)(1, %5)
│ %7 = Base.vect(1, "123", %6)
│ @_2 = Base.iterate(%7) ; % 迭代器
│ %9 = @_2
│ %10 = %9 === nothing
│ %11 = Base.not_int(%10)
└── goto #4 if not %11
2 ┄ %13 = @_2 ; % ==循环体==
│ i = Core.getfield(%13, 1) ; % 循环变量
│ %15 = Core.getfield(%13, 2) ; % 循环指数
│ %16 = Main.:+
│ %17 = a
│ a = (%16)(%17, 1)
│ %19 = i
│ %20 = a
│ Main.println(%19, %20)
│ @_2 = Base.iterate(%7, %15)
│ %23 = @_2
│ %24 = %23 === nothing
│ %25 = Base.not_int(%24)
└── goto #4 if not %25
3 ─ goto #2
4 ┄ return nothing
)
Core._apply_iterate(Base.iterate, 函数, 容器1, 容器2, ...) 会依次把每个 容器 的每个元素作为入参去调用 函数。其中 Base.iterate 是一个函数,用途未知。
Base.not_int(变量) 逻辑非运算(似乎非逻辑类型就按 bit 取非)
Base.Pairs(("a","b"), (1,2)) 返回的类型是 Base.Pairs{Int64, String, Tuple{Int64, Int64}, Tuple{String, String}},但类似于 Dict([1=>"a", 2=>"b"])。可以使用 var[key] 获取对应的值。
Base.iterate(容器) 可以得到 容器 的初始迭代器。Base.iterate(容器, 迭代器) 可以得到下一个元素的迭代器。
Main 模块的,例如 Main.println,Main.error,Main.sqrt 等。
Main.:算符符号,如大于号 Main.:>,减号 Main.:-,不等于 Main.:!=。1:5 中的冒号表示为 Main.:(:)
@code_lowered 会调用 C:\Users\addis\AppData\Local\Programs\Julia-1.11.6\share\julia\base\reflection.jl 中的 function code_lowered(@nospecialize(f), @nospecialize(t=Tuple); generated::Bool=true, debuginfo::Symbol=:default)
reflection.jl 的 function method_instances(@nospecialize(f), @nospecialize(t), world::UInt) 会返回 Core.MethodInstance 数组,每个元素是 Core.Compiler.specialize_method(match) 返回的。其 source 字段应该就是压缩后的 IR
reflection.jl 的 uncompressed_ir(m::Method) 最后会调用 ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), m, C_NULL, s)::CodeInfo,也就是调用 c 语言函数。
@code_lowered 返回一个 Core.CodeInfo 类型的结构体,最后经过一次 remove_linenums!(code),打印出来就是我们看到的文本格式 Julia IR,其 code 字段(Vector{Any})就是内部的数据结构。
%数字 并不是连续的,而是行号也就是 code[数字]
Expr, GlobalRef(例如 Main.:+), Core.SlotNumber(变量名赋值给临时变量), Core.NewVarNode, Core.GotoIfNot(goto 语句)
code 中用符号 :_数字 来表示,具体的变量名对照表在 slotnames 字段中 slotnames[数字]。
fieldnames(GlobalRef) 为 (:mod, :name, :binding),是为了引用某个模块中的某个全局对象。mod::Module 字段是模块名,name::Symbol,是被引用的符号名,binding::Core.Binding 未知。
源码分析
Core.CodeInfo 类型的
code 字段是 Vector{Any},元素类型有 Expr, Nothing, Core.ReturnNode(表示 return)。其中 Expr 的 head 一般是 :invoke,args 分别是被调函数的 MethodInstance,和被调函数的 GlobalRef(例如 Main.print),函数入参 1,入参 2,……
Core.ReturnNode 只有一个 val 字段,就是返回值。
Core.PhiNode
Core.PiNode,有两个字段 val, typ
boot.jl 中注释了许多基本类型的定义,这些类型是使用 C 语言定义的。如
mutable struct Expr
head::Symbol
args::Array{Any,1}
end
struct GotoNode
label::Int
end
struct PiNode
val
typ
end
mutable struct Symbol
# opaque (不透明对象)
end
Core.eval(模块, 表达式) 可以在某个模块中执行表达式,而普通的 Main.eval(x) = Core.eval(Main, x) 默认在 Main 模块(sysimg.jl)。
Base 模块中的 add_int, or_int, xor_int, mul_float, lt_float, setfield!, getfield,他们的类型是 Core.IntrinsicFunction。
make debug(文档)。注意 make 的之前可能需要挂个梯子。
G:\OneDrive\github\etc-3rd-party\julia\usr\bin\julia-debug。
-e 'println("Hello World!")'
rm -rf build && mkdir build
make O="$PWD/build" configure
make debug 失败,提了 issue 正在修复。撤回到 v1.10.10 编译成功。最新:2025-11-15 的 89243d1cdf 编译成功(开始了用 jl 重写 lower 代码)。
make debug 之前最好把 .git 文件夹之外的东西都删掉然后重新 git checkout .
origin
JULIA stdlib/SparseArrays.debug.image 等镜像时可能会报 Segmentation Fault,再次运行 make debug 会继续。
julia -e '命令;命令' 可以运行命令而不互动
Expr 的 C 类型为
typedef struct {
JL_DATA_TYPE
jl_sym_t *head;
jl_array_t *args;
} jl_expr_t;
可以直接把 jl_value_t * 硬 cast 成 jl_expr_t *。
Symbol 的 C 类型为
typedef struct _jl_sym_t {
JL_DATA_TYPE
_Atomic(struct _jl_sym_t*) left; // 左子节点
_Atomic(struct _jl_sym_t*) right; // 右子节点
uintptr_t hash; // precomputed hash value
// JL_ATTRIBUTE_ALIGN_PTRSIZE(char name[]);
} jl_sym_t;
大概就是用二叉树实现了一个 std::set,用作 StringPool。但注意这里只不会储存 string 本身只会储存它的 hash。所以 sym 不知道他自己的名字。
jl_symbol_name() 可以获取 symbol 的名字(可以直接在 debugger 中使用),原理是寻找 jl_sym_t 对象在内存中后面的地址并转为 char*(后面到底是什么?)
ast.c 中定义了(所有可能的?)表达式类型,即 jl_expr_t 的 head。它们的 jl_symbol_name() 是两个下划线之间的名字。
// head symbols for each expression type
JL_DLLEXPORT jl_sym_t *jl_call_sym;
JL_DLLEXPORT jl_sym_t *jl_invoke_sym;
JL_DLLEXPORT jl_sym_t *jl_invoke_modify_sym;
JL_DLLEXPORT jl_sym_t *jl_empty_sym;
JL_DLLEXPORT jl_sym_t *jl_top_sym;
JL_DLLEXPORT jl_sym_t *jl_module_sym;
JL_DLLEXPORT jl_sym_t *jl_slot_sym;
...
julia.h 是 Julia C API 的总头文件,在 C/C++ 中调用 julia 时用它(类似于 Python.h)。
jl_value_t 本身没有定义,gc.c:jl_gc_alloc_() 分配的内存布局为:jl_taggedvalue_t + 数据部分。
返回的 jl_value_t* 就是 数据部分 的起始指针,jl_taggedvalue_t 存放的就是动态类型信息。
jl_value_t 具体是什么?可以用 julia.h 中的 jl_is_类型() 宏函数。如果函数名是 jl_is_*node() 那么就是用于判断是否为语法树节点。例如 jl_is_linenode 判断是否为标记文件和行号的节点。
jl_is_类型() 实现上是通过 jl_typetagis() 宏函数。对比指针指向的一个 bit field,和 jl_类型_tag(如 jl_int16_tag),在 enum jl_small_typeof_tags 中定义。
jl_typetagof(对象) 宏函数返回 jl_value_t 对象的 typetag
jl_typeof(对象) 返回另一个 jl_value_t,类型是 DataType。
Array 的 C 类型为
typedef struct {
JL_DATA_TYPE
void *data;
size_t length; // 矩阵元个数
jl_array_flags_t flags;
uint16_t elsize; // element size including alignment (dim 1 memory stride)
uint32_t offset; // for 1-d only. does not need to get big.
size_t nrows;
union {
// 1d
size_t maxsize;
// Nd
size_t ncols;
};
// other dim sizes go here for ndims > 2
// followed by alignment padding and inline data, or owner pointer
} jl_array_t;
jl_array_len() 可以获取长度,ajl_array_ptr_ref() 可以获取矩阵元
julia.h 中 jl_datatype_t *jl_expr_type; 这说明 _type 结尾的是一个值,_t 结尾的才是类型。
jl_is_expr 如何定义?
__attribute__((constructor)) 开头的函数会在库加载时被调用(有可能在 main() 前,如果不使用 dlopen() 加载。
jlapi.c:true_main() 函数会加载 Base._start,然后 jl_apply() 会调用该函数(位于 C:\Users\addis\AppData\Local\Programs\Julia-1.11.7\share\julia\base 的 client.jl)。
gc.c:_jl_invoke() 中的 invoke() 函数时无法继续步入,可能因为这是从 jl 由 llvm 编译成的机器码,没有 debug info。在 Julia debugger 中打断点也没用,好像只能 print debug
julia -e 'println("hello!")' 最终会在 parse_input_line(::String) 进而 Meta.parseall() 中进行词法语法解析,`Core.eval(Main, ex) 中执行。
Meta.parseall() 会调用 _parse_string(),Core._parse(),parsing.jl:fl_parse(),最后调用 C 语言的 src/ast.c:jl_fl_parse 但该函数在 gdb 中同样无法断点暂停。
fl_ 指的是 femtolisp
ast.c 中 jl_parse*() 函数都会调用 jl_parse(),进而调用上述的 Core._parse()。可见,无论是 -e 还是 parse_input_line 都没有使用 JuliaSyntax.parse(目前是 1.11.6 > 1.10)
jl_fl_parse() 里面调用一堆
jl_fl_parse() 负责调用 jlfrontend.scm 中的 jl-parse-all或one 函数,把 jl 源码字符串 parse 成 S-expr 再转换为 Julia 的 Expr 类型。
jl_task_t 是 Julia 的 Task,也叫协程(coroutines),也叫绿线程。携程不是操作系统层面的线程,而是 julia runtime 实现的。
jl_task_t::world_age 决定了它可以看到哪些版本的方法。例如:
f(x) = 1
@async begin
sleep(1)
f("hello") # always return 1
end
f(x::String) = 2 # defined later
Core.eval(),调用 toplevel.c: jl_toplevel_eval_flex() 它内部会通过 jl_needs_lowering() 判断是否直接执行语法树?
jl_toplevel_evel_flex() 的节点是 jl_toplevel_sym,要递归对每个分支执行 jl_toplevel_evel_flex()
body_attributes() 用于总结要执行的 Expr 的一些属性,例如是否有函数定义,是否有 ccall(),然后用 if, else 立即判断是否需要编译该 AST,还是使用解释器直接执行。
jl_interpret_toplevel_thunk()。注意第二个参数 jl_code_info_t src 中含有 SSA。(是否执行 SSA 而不是语法树?),jl_array_t *stmts = src->code;(类型必须为 Array{Any})就是所有 IR 命令?
jl_interpret_toplevel_thunk() 会调用 interpreter.c: eval_body()
do_call() 求函数调用
eval_value() 求其中一个参数
typedef struct {
jl_code_info_t *src; // contains the names and number of slots
jl_method_instance_t *mi; // MethodInstance we're executing,
// or NULL if toplevel
jl_module_t *module; // context for globals
jl_value_t **locals; // slots for holding local slots and ssavalues
jl_svec_t *sparam_vals; // method static parameters,
// if eval-ing a method body
size_t ip; // Leak the currently-evaluating statement
// index to backtrace capture
int preevaluation; // use special rules for pre-evaluating expressions
// (deprecated--only for ccall handling)
int continue_at; // statement index to jump to after leaving
// exception handler (0 if none)
} interpreter_state;
typedef struct _jl_module_t { // Julia 模块
JL_DATA_TYPE
jl_sym_t *name; // 模块名
struct _jl_module_t *parent;
_Atomic(jl_svec_t*) bindings;
_Atomic(jl_array_t*) bindingkeyset; // index lookup by name into bindings
// hidden fields:
arraylist_t usings; // modules with all bindings potentially imported
jl_uuid_t build_id;
jl_uuid_t uuid;
size_t primary_world;
_Atomic(uint32_t) counter;
int32_t nospecialize; // global bit flags: initialization for new methods
int8_t optlevel;
int8_t compile;
int8_t infer;
uint8_t istopmod;
int8_t max_methods;
jl_mutex_t lock;
intptr_t hash;
} jl_module_t;
Main.println 函数的时候,println 只是一个 symbol 而不是函数对象,jl_eval_global_var()(调用 jl_get_global())可以从 Main 中获取函数对象。
println 的函数对象和所有求值后的参数被传入 jl_apply() 调用 jl_apply_generic() 调用 _jl_invoke()
b = 2; for i in 1:10000 println("(b+i)^2 = ", (b+i)^2) end
./flisp mk_julia_flisp_boot.scm src/ jlfrontend.scm src/julia_flisp.boot
jlfrontend.scm 才是整个 fl 前端的入口
julia_flisp.boot.inc(其实就是将 boot 文件的内容编码成一个 16 进制整数数组),这个头文件将被 include 到 C 代码中一起被编译。
jl_apply_generic() julia_apply() 通过函数对象和入参调用函数。但 invoke() 则是更底层的,调用的是最终的实例(可能是 JIT 编译出的函数指针)。
f(x::Int, y::Float64) 的 signature 是 Tuple{Int, Float64},f(x::Number, y) 的是 Tuple{Number, Any}。leafsig(leaf signature)的意思是所有类型都是具体类(isconcrete()),也就是类型树中的叶子节点。
jl_methtable_t *jl_gf_mtable(jl_value_t *F) 返回函数对象 F 的分派表。
jl_methtable_t 是方法表,里面每一个方法是一个 jl_method_t,对应 jl 源码中的一个 function 关键字。每个方法还会细分为 jl_method_instance_t,也就是每个类型都最终确定为具体类的最终特化。
gf.c)jl_method_instance_t * jl_lookup_generic_(jl_value_t *F, jl_value_t **args, uint32_t nargs, uint32_t callsite, size_t world); 根据函数对象,入参,以及调用该函数处的指针,world_age 查找并返回最终特化。
jl_method_instance_t * 后,调用该方法实例用(gf.c)jl_value_t *_jl_invoke(jl_value_t *F, jl_value_t **args, uint32_t nargs, jl_method_instance_t *mfunc, size_t world)
jl_code_instance_t(链表),这是因为可能有不同的 world age 和 compilation states。每一个 jl_code_instance_t 的 next 字段指向下一个,jl_code_instance_t 的 invoke 字段(类型 jl_callptr_t,定义为 typedef jl_value_t *(jl_call_t)(jl_value_t*, jl_value_t**, uint32_t, struct _jl_code_instance_t*);,使用方法 函数调用(入参,出参,入参个数,函数实例))是调用该 code instance 的函数指针,可以直接用 如果参数类型等不匹配导致调用失败,会返回 NULL。
那该怎么调试呢?
JuliaInterpreter.@interpret f(args...) 可以强制使用解释器执行函数。(debugger 用的就是这个)
julia --compile=min或no。即可。
 
 
 
 
 
 
 
 
 
 
 
友情链接: 超理论坛 | ©小时科技 保留一切权利