# 用例驱动代码生成:支持类型不稳定的 Julia 代码
代码生成工具支持通过用例驱动的方式编译类型不稳定的代码
# 类型不稳定导致的代码无法正确运行
Julia 代码中的类型不稳定问题,是代码生成失败的主要原因。在实际编写代码时,类型不稳定问题体现为动态调用 (详见代码生成的 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
因为编译器无法推断 arr[1] 的具体类型,所以 f(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 文件,内容如下:
# main.jl
struct A end
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
f(arr[1]) # 运行阶段触发错误
f(arr[2])
end
include("test.jl") # 通过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)
f(::Float64)
# 限制方法实例的收集
目前,代码生成工具在方法实例的收集上存在局限性,无法根据用例精准的收集方法实例,且默认情况下不限制特定函数的方法实例的数量。
以加法函数 + 为例:
using MethodAnalysis # 分析方法实例的 julia 工具库
methodinstances(+)
即使用户还没有提供任何加法函数 + 的用例,编译器仍然将分析得到共151个方法实例(此数量将随执行环境的不同而有所不同):
151-element Vector{Core.MethodInstance}:
MethodInstance for +(::Int64, ::Int64)
MethodInstance for +(::UInt32, ::UInt32)
MethodInstance for +(::UInt64, ::UInt64)
MethodInstance for +(::Int32, ::Int32)
MethodInstance for +(::UInt8, ::UInt8)
MethodInstance for +(::UInt128, ::UInt128)
MethodInstance for +(::Int128, ::Int128)
MethodInstance for +(::UInt16, ::UInt16)
MethodInstance for +(::Float64, ::Float64)
MethodInstance for +(::Float16, ::Float16)
MethodInstance for +(::Float32, ::Float32)
MethodInstance for +(::Int64, ::BigFloat)
MethodInstance for +(::P, ::P) where P<:Dates.Period
MethodInstance for +(::Char, ::Int64)
MethodInstance for +(::Char, ::UInt8)
MethodInstance for +(::AbstractChar, ::UInt8)
MethodInstance for +(::Ptr{Any}, ::Int64)
MethodInstance for +(::Ptr{Expr}, ::Int64)
MethodInstance for +(::Ptr{QuoteNode}, ::Int64)
MethodInstance for +(::Ptr{Symbol}, ::Int64)
MethodInstance for +(::Ptr{UInt8}, ::Int64)
MethodInstance for +(::Ptr{Ptr{Nothing}}, ::Int64)
⋮
julia 运行方式类似 C++ 模板,会为同一函数的不同参数类型生成不同的方法实例。 当编译器发现一例函数调用,且无法推断其参数类型时,将在运行阶段根据实际类型查找并跳转至对应的方法实例。
默认选项下,代码生成工具不限制任一函数生成的方法实例的数量,
因此,当用户动态调用了 + 时,代码生成工具将会尝试对上述的151个方法实例进行代码生成。
这将导致生成代码规模急剧膨胀,与此同时,上述151个方法实例中可能存在编译器无法生成的方法实例,此时代码生成工具抛出编译期错误。
因此,为防止无关代码阻塞代码生成过程,代码生成工具提供以下功能作为解决方案。
# 禁用特定方法实例
当前代码生成工具存在冗余用例搜集问题,这些冗余用例可能访问未受支持的Julia代码(参考功能支持范围),进而导致程序无法编译或运行出错。 此小节介绍如何通过禁用特定方法实例解决上述问题。
# main.jl
function main()
arr = []
push!(arr, 1.5)
push!(arr, 1)
println(arr[1])
println(arr[2])
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
println(1.5)
println(1)
if @isdefined(SyslabCC)
SyslabCC.block_method_instance(::Type{Tuple{typeof(println),Core.MethodInstance}}) = false
# 用法:
# SyslabCC.block_method_instance(::Type{Tuple{函数的类型,参数1的类型, ..., 参数N的类型}}) = false
end
通过用例驱动代码生成编译代码:
scc main.jl -o main --collect-instance
此时,代码生成工具报错:
Compiler Error(3): ccall on julia's c function which is not implemented: 'jl_uncompress_argnames'
...
in the body of typeof(println), with (Core.MethodInstance) as its arguments type
(dynamic call detected: you may block it with 'SyslabCC.block_method_instance(...)')
...
阅读上述报错信息可知,代码生成工具仍然收集到了冗余方法实例 println(::Core.MethodInstance),即使用户未在test.jl中提供产生该方法实例的用例。
此时可以用 SyslabCC.block_method_instance 禁用 println(::Core.MethodInstance):
# main.jl
function main()
arr = []
push!(arr, 1.5)
push!(arr, 1)
println(arr[1])
println(arr[2])
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
println(1.5)
println(1)
@static if @isdefined(SyslabCC)
SyslabCC.block_method_instance(::Type{Tuple{typeof(println),Core.MethodInstance}}) = true
# 用法:
# SyslabCC.block_method_instance(::Type{Tuple{函数的类型,参数1的类型, ..., 参数N的类型}})
end
再次使用用例驱动选项编译并运行代码:
scc main.jl -o main --bundle --static-mingw --collect-instance # 成功编译
.\main.exe
# Linux 命令
# scc main.jl -o main --bundle --collect-instance
# ./main
此时可以看到代码被正确执行:
1.5
1
提示
代码生成工具已经为用户预先禁用了部分方法实例,其中包括上文中提到的 println(::Core.MethodInstance),
因此用户即使没有显式禁用 println(::Core.MethodInstance),也可以正常编译并运行代码。
# main.jl
function main()
arr = []
push!(arr, 1.5)
push!(arr, 1)
println(arr[1])
println(arr[2])
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
println(1.5)
println(1)
使用用例驱动选项编译并运行代码:
scc main.jl -o main --collect-instance # 成功编译
.\main.exe # Linux 下使用 ./main 运行
此时可以看到代码被正确执行:
1.5
1
提示
禁用方法实例只会影响代码生成对动态调用的处理,不影响静态调用
例如:
# main.jl
struct A end
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
f(A()) # 静态调用
f(arr[1]) # 动态调用
f(arr[2]) # 动态调用
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
SyslabCC.block_method_instance(::Type{Tuple{typeof(f),A}}) = true
f(1.5)
通过用例驱动编译并运行程序:
scc main.jl -o main --bundle --static-mingw --collect-instance # 成功编译
.\main.exe
# Linux 命令
# scc main.jl -o main --bundle --collect-instance
# ./main
f(::Any)
Error: ErrorException("No method instance collected for methods at dynamic call to typeof(f). Try to increase limit of dynamic dispatch by 'SyslabCC.dispatch_limit', or make call site type stable to avoid dynamic call.")
可以看即使禁用了方法实例f(::A), 第一次静态调用f(A())仍然被正确执行,
直到运行动态调用 f(arr[1]) 时才会抛出异常。
# 限制方法实例收集数量
用户也可以通过SyslabCC.dispatch_limit限制方法实例收集数量,从而粗略的禁用方法实例,快速上手用例驱动的代码生成。例如:
# main.jl
struct A end
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
f(A()) # 静态调用
f(arr[1]) # 动态调用
f(arr[2]) # 动态调用
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
f(A())
f(1.5)
if @isdefined(SyslabCC)
SyslabCC.dispatch_limit(::Type{typeof(f)}) = 2 # 限制f的方法实例收集的数量上限为2
# 用法:
# SyslabCC.dispatch_limit(::Type{函数的类型}) = 该函数收集的方法实例的数量上限
end
也可以通过命令行选项 --dispatch-limit 一次性限制所有方法实例的数量:
# 限制所有方法的用例收集数量上限为2
scc main.jl -o main --bundle --static-mingw --collect-instance --dispatch-limit 2 -c
加入 -c 选项仅生成代码,运行并编译命令,可跳过耗时的 C++ 编译过程。
scc 编译器运行期间,代码生成过程出现如下警告(可能因执行环境不同而有所不同):
warning: The number of found method instances for `typeof(string)` exceeds the user configured limit!
User configured 2 but found 42 dispatchable target.
警告说明,程序中出现 string 函数的动态调用,共搜集 42 个方法实例,此时,若代码执行路径上存在 string 函数的动态调用,程序将在运行阶段抛出错误。
用户可根据警告将 --dispatch-limit 选项提升到 42 (或用户环境下警告建议的其他相应数量),则明显提升程序处理动态调用的能力。
然而,设置过高的 --dispatch-limit 选项将引起生成代码规模急剧膨胀,因此建议用户参考警告信息、根据实际情况逐步提升 --dispatch-limit 选项的取值。
在此案例中,强制指定 f 的方法实例收集数量上限为2,即使用户未提供 f(::A) 的用例,代码生成工具也能正确编译并运行代码。
# 取消 -c 选项,调用 C++ 编译器输出可执行文件
scc main.jl -o main --bundle --static-mingw --collect-instance --dispatch-limit 2
.\main.exe
# Linux 命令:
# ./main
f(::Any)
f(::Any)
f(::Float64)
# 用例驱动的代码生成的局限性
目前,用例驱动生成主要用于处理用户代码的动态调用。 用例驱动生成可能难以处理上游 Julia 库中的动态调用,因为需要通过 SyslabCC.block_method_instance 手动禁用大量方法实例。此状况将在未来代码生成支持精准用例搜集后,才能得到有效缓解。
# 只有被收集到的方法实例能被动态调用
用例驱动生成需搜集用例涉及的全部方法实例,否则仍会在运行阶段抛出错误。例如,对于上述示例中的 main.jl 文件,如果未提供用例 f(A()):
# main.jl
struct A end
f(::Float64) = println("f(::Float64)")
f(::Int) = println("f(::Int)")
f(::String) = println("f(::String)")
f(::Any) = println("f(::Any)")
function main()
arr = []
push!(arr, A())
push!(arr, 1.5)
f(arr[1]) # 动态调用
f(arr[2]) # 动态调用
end
include("test.jl") # 通过include执行用例文件 test.jl
# test.jl
f(1.5)
则仍然会在运行阶段触发错误:
Error: ErrorException("No method instance collected for methods at dynamic call to typeof(f). Try to increase limit of dynamic dispatch by 'SyslabCC.dispatch_limit', or make call site type stable to avoid dynamic call.")
# 无法支持原语函数的动态调用
目前,用例驱动的代码生成无法支持 Julia 原语函数的动态调用。因此即使开启了用例驱动选项,亦无法支持原语函数的动态调用,如 getfield, apply_iterate。
提示
可以检查函数类型是否为 Core.IntrinsicFunction 或 Core.Builtin,来判断是不是 Julia 原语函数。例如:
getfield isa Core.Builtin # 通过 isa 运算符判断函数是否为 Core.Builtin 类型
true
# 无法精准收集用例
目前代码生成工具尚无法精准收集用户执行到的函数实例,因此,当启动用例驱动后,这些收集到的“冗余用例”将导致编译的方法数量大于用户实际的执行的方法数量。