# 代码生成的局限性


Syslab 代码生成工具支持将 Julia 程序编译为可执行的二进制文件、动态库和 C++ 源码项目。 在代码生成的过程中,会将 Julia 视为静态编译型语言以充分发挥 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()

在上述代码中,我们做了两件事情以支持代码生成:

  1. tuple 原语调用封装至 make_tuple 函数内,此时在程序中只存在对 make_tuple 函数的动态调用。
  2. 在程序中提供了用例 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