# 代码生成的局限性
Syslab 代码生成工具支持将 Julia 程序编译为可执行的二进制文件、动态库和 C++ 源码项目。 在代码生成的过程中,会将 Julia 视为静态编译型语言以充分发挥 Julia 的性能上限, 因此对于包含动态特性的某些场景,当前的代码生成可能无法完全支持。 对于这些场景,部分可以直接通过用例驱动的代码生成支持,而对于其他场景则需要手动调整代码以适配代码生成工具。
# 大多数的类型不稳定的函数调用
Syslab 代码生成工具要求待生成的代码的所有调用都是类型稳定的,即不能出现动态调用。 当类型不稳定的函数调用出现时,代码生成仍能正常进行,但是在执行到该类型不稳定的代码时会触发运行时错误。例如:
# main.jl
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
struct A end
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
push!(arr, 1)
push!(arr, "hello")
f(arr[1])
end
对于以上代码,由于 Julia 无法推断arr[1]的类型,因此如果不加任何选项编译,程序将在运行阶段触发错误:
scc main.jl -o main --bundle --static-mingw # 成功编译
.\main.exe
# Linux 命令
# scc main.jl -o main --bundle
# ./main
在运行阶段产生以下错误:
Error: ErrorException("dynamic call(Main.f, %14::Any)")
此时,请参见用例驱动代码生成:支持类型不稳定的 Julia 代码章节, 通过提供用例的方式驱动代码生成,以支持存在类型不稳定的代码的生成。
# main.jl
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
struct A end
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
push!(arr, 1)
push!(arr, "hello")
f(arr[1])
end
include("test.jl") # 手动提供用例以使用用例驱动的代码生成
# test.jl
f(A())
f(1.5)
然后通过 --collect-instance 选项以使用用例驱动的代码生成:
scc main.jl -o main --bundle --static-mingw --collect-instance # 成功编译
.\main.exe
# Linux 命令
# scc main.jl -o main --bundle --collect-instance
# ./main
成功运行得到结果:
f(::Any)
# 未定义变量的使用
注意
该场景需要用户手动修改源代码以适配代码生成。
代码生成工具要求待生成代码的所有变量均被定义,如果待生成的函数存在未定义的变量,无论该变量所在语句是否会被执行, 代码生成工具都将在编译期报错。例如:
# under_var.jl
function always_true()
return rand() > 0
end
function main()
if always_true()
println("Hello Syslab!")
else
println(x)
end
end
对于上述代码,即使 main 函数的条件语句 else 分支不会被执行到,代码生成仍然会在编译期抛出错误。
Compiler Error(4): undefined variable `x`
in the body of typeof(main), with () as its arguments type
File <path-to-script>/undef_var.jl, line 10, in Main.main
未定义变量的使用在 Julia 是不合法的用法,代码生成工具会将这种用法识别为错误并抛出。 此时用户需要检查代码是否存在使用了未定义变量的问题,如:是否忘记导入了包含该变量定义的模块。 手动在代码中提供变量定义,或者避免引用未定义变量。
# 全局变量非常量
注意
该场景需要用户手动修改源代码以适配代码生成
代码生成要求全局变量全部为 Julia 的常量,例如:
# main.jl
x = 1
function main()
global x
println(x)
end
上述代码的 main函数中存在对于非常量的全局变量 x 的使用,因此此时代码生成将在编译阶段报错:
Compiler Error(4): non-constant global variable `x`
in the body of typeof(main), with () as its arguments type
File <path-to-script>/main.jl, line 6, in Main.main
此时用户可以通过 Ref 类型对全局变量进行封装,在提供可变性的同时保证全局变量为常量:
提示
Julia 的引用类型 Ref 类似于 C 语言中的指针,用户可以通过运算符 [] 对引用进行解引用操作,例如:
const r = Ref(1) # 创建一个引用 r
println(r[]) # 通过解引用操作 r[],访问引用 r 的指向的值 1
r[] = 20 # 通过解引用操作 r[],将引用 r 指向的值修改为 20
println(r[]) # 通过解引用操作 r[],访问引用 r 的指向的值 20
通过 Ref 将变量包装成引用类型,可以保证变量的类型签名为 const 的同时,保持修改变量的能力。
将全局变量通过 Ref 类型封装并标记为 const 后,可以得到如下程序,该程序可以被代码生成工具成功编译且编译后可以正确运行:
const x = Ref{Int}(1)
function main()
global x
println(x[])
x[] = 2
println(x[])
end
# 类型不稳定的 Julia 原语调用
注意
该场景需要用户手动修改源代码以适配代码生成
目前,代码生成工具尚不支持部分 Julia 原语函数的类型不稳定调用。
例如,代码生成要求 tuple 原语的调用必须是类型稳定的,因此当 Julia 无法推断 tuple 的参数类型时,代码生成将报错:
# main.jl
const x = Ref{Vector{Any}}([])
function main()
push!(x[], 1)
t = tuple(x[][1])
println(t)
end
提示
元组表达式 (x, y, ...) 在 Julia 底层会被翻译为 tuple 原语的调用。
对于上述代码,由于 Julia 无法推断全局向量 x 的元素类型,对其进行代码生成会报出如下错误:
Compiler Error(2): tuple(): 1-th argument is not statically typed
in the body of typeof(main), with () as its arguments type
File <path-to-script>/main.jl, line 5, in Main.main
该报错的含义是,在代码生成的时候无法推断 tuple 原语调用的第 1 个参数,对于这种存在原语函数动态调用的场景,
我们推荐用户手动封装原语函数,从而将动态性隔离在函数调用层面,并结合用例驱动的代码生成支持动态地使用原语函数。
# main.jl
const x = Ref{Vector{Any}}([])
# 将原语封装为用户函数,将动态性隔离在函数调用层面,并通过 @noinlnie 宏防止该函数被内联
@noinline make_tuple(x) = tuple(x)
function main()
push!(x[], 1)
t = make_tuple(x[][1])
println(t)
end
include("test.jl") # 手动提供用例以使用用例驱动的代码生成
# test.jl
main()
在上述代码中,我们做了两件事情以支持代码生成:
- 将
tuple原语调用封装至make_tuple函数内,此时在程序中只存在对make_tuple函数的动态调用。 - 在程序中提供了用例
main()。
此时可以通过用例驱动的方式进行代码生成:
scc main.jl --collect-instance -o main
./main
程序正常运行:
(1)
# 动态操作类型
注意
该场景需要用户手动修改源代码以适配代码生成。
正如之前提到的,代码生成会将 Julia 视为一门静态编译型语言,
因此对部分直接对类型的操作,代码生成仅提供了有限的支持,例如:
代码生成要求所有原语函数 typeof 的调用必须返回具体类型,这会导致部分将类型视为一等公民的使用的代码在编译期报错:
# main
function bad(data::Vector{Union{Int64, Float64}})
x = data[1]
T = typeof(x)
if T isa Type{Int}
println("data[1] is a Int")
elseif T isa Type{Float64}
println("data[1] is a Float64")
end
end
function main()
bad(Union{Int64, Float64}[1])
end
对于上述代码,代码生成将在编译期报错:
Compiler Error(1): typeof must return concrete type
in the body of typeof(main), with () as its arguments type
File <path-to-script>/main.jl, line 15, in Main.bad
File <path-to-script>/main.jl, line 23, in Main.main
对于这种场景,用户需要将代码修改为直接在数据上进行操作的形式,避免对直接操作类型对象:
# main.jl
function good(data::Vector{Union{Int64, Float64}})
x = data[1]
if x isa Int
println("data[1] is a Int")
elseif x isa Float64
println("data[1] is a Float64")
end
end
function main()
good(Union{Int64, Float64}[1])
end
编译并运行代码:
scc main.jl -o main
./main
程序正常运行:
data[1] is a Int