2026b

# 用例驱动代码生成:支持类型不稳定的 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

# 无法精准收集用例

目前代码生成工具尚无法精准收集用户执行到的函数实例,因此,当启动用例驱动后,这些收集到的“冗余用例”将导致编译的方法数量大于用户实际的执行的方法数量。