# 错误诊断


关于使用 scc 命令行工具时如何进行错误诊断

报错信息分为运行时报错和编译期报错。

对于运行时报错可以使用 --debugtrace 选项,可执行文件在触发运行时错误时同时会输出对应 Julia 源码的调用栈信息。

编译期报错会在编译时抛出,并给出源码定位信息,还可以使用 --verbose 编译选项,在编译期间打印出更详细的编译信息,方便调试定位。

此章节介绍一般性的错误诊断,对于其他专门类型的错误,解决方案如下:

# 使用 --debugtrace 定位运行时错误

运行时错误如动态调用、数组越界,不会在编译期输出。

而默认模式下编译的可执行文件,在触发运行时错误时,只会给出错误的简短信息,无法定位到具体的 Julia 源代码。

使用 --debugtrace 编译选项,可以在生成的代码中插入 Julia 源代码的调用栈信息。并在触发运行时错误时,输出对应的调用栈信息,以便定位问题。

    Flags:
        ...
        --debugtrace       : Report Julia's stack trace when runtime error occurs.

此处给出一个输出运行时错误的调用栈信息的示例。

新建 main.jl 文件,输入以下内容:

@noinline function mysqrt(x)
    y = sqrt(2x)
    println("sqrt(2*x) = ", y)
end

function main(args::Vector{String})
    if length(args) == 1
        mysqrt(parse(Float64, args[1]))
    else
        println("invalid args")
    end
end

@isdefined(SyslabCC) || main(ARGS)
scc .\main.jl -o main --bundle --debugtrace
.\main.exe -2
# Linux 命令:
# scc ./main.jl -o main --bundle --debugtrace
# ./main -2

运行结果为:

Error: ErrorException("sqrt will only return a complex result if called with a complex argument.")
stacktrace:
    error.jl:35 at error()
    <path>\basics.jl:33 at PatchHandler()
    math.jl:677 at sqrt()
    <path>\main.jl:2 at mysqrt()
    <path>\main.jl:8 at main()

stacktrace 中的信息显示在 main.jl 文件的第 2 行,函数 mysqrt 中抛出了错误。

提示

--debugtrace 编译选项会带来额外的性能和存储开销。

建议在运行时错误解决后,在性能敏感的部署环境中禁用 --debugtrace 编译选项,重新生成代码。

# 通过编译器报错定位错误

代码生成需要 Julia 源代码的类型稳定,进一步解释可以参见:编写类型稳定的代码

此处给出一个类型不稳定的代码示例。

新建 main.jl 输入以下代码:

function main(n)
    x = [] # 初始化了一个元素类型为 Any 的数组
    push!(x, 1)
    push!(x, 2.0)
    x[1] + x[2] # 此处发生类型不稳定,无法推断 x[i] 的具体类型
end

@static @isdefined(SyslabCC) || main()

接下来会演示,如何通过编译器报错定位源代码中类型不稳定问题。

以上代码,在使用 scc 命令行工具编译时,加上 --full-aot ALL 选项,编译器会抛出如下错误:

scc main.jl -o main --bundle --no-blas --full-aot ALL
...
Compiler Error(6): dynamic call(Main.+, %8::Any, %9::Any): invoke(typeof(+),Any,Any) failed
in the body of typeof(main), with (Vector{String}) as its arguments type
    File <path>\main.jl, line 5, in Main.main

查看报错信息,可以观察到此次代码生成失败是因为 main.jlline 5 位置发生了动态调用,原因是 x = [] 会生成元素类型为抽象类型的数组 Any[],导致后续调用 s += x[i] 时无法推断 x[i] 的具体类型,可以在数组声明时使用 T[] 语法来消除类型不稳定:

function main(n)
    x = Float64[] # 对空数组标注元素类型
    push!(x, 1)
    push!(x, 2.0)
    x[1] + x[2] # 类型稳定,x[i] 推断为 Float64 类型
end

@static @isdefined(SyslabCC) || main()
scc main.jl -o main --bundle --no-blas --full-aot ALL
.\main.exe # Linux 下使用 ./main 运行

没有报错输出表示程序正确运行。

对于消除代码中的动态调用的解决思路,主要有以下两种:

  1. 检查是否确实需要抽象字段和抽象类型,推荐从根本上避免动态调用;
  2. 如果场景确实需要抽象类型,可以通过 isa 判断具体类型,在对应分支可生成静态代码。

对动态调用代码的进一步处理可以参考:

提示

并非所有的动态调用都需要修改,如果代码并不会实际运行到动态调用的分支,例如抛出错误的分支,那么这个动态调用一般是不需要处理的。

如果在实际运行中触发了动态调用的错误,也可以使用 --debugtrace 编译选项,在运行时检查错误的来源。

另外,对于熟悉 Julia IR 的开发者,可以使用 --debug 选项,输出代码生成过程中的 Julia IR 信息,用于定位复杂的类型不稳定问题。

    Flags:
        ...
        --debug     : Toggle developer debug information.

# 使用 --verbose 输出详细信息

使用 --verbose 编译选项可以在编译期间打印出更详细的编译信息,有助于调试定位。

代码生成所需时间与 Julia 代码和宿主机性能相关,如果时间较长,可以使用 --verbose 编译选项,确认代码生成执行情况。

在以下示例中,关于 fft 的编译时间较长,此处可以通过 --verbose 的编译选项确认代码生成过程正常执行。

新建 main.jl 文件:

using TyMathCore

function main()
    A = 1:10000;
    A = reshape(A, 100, 100)
    display(fft(A)[1, 1:2])
end

@static @isdefined(SyslabCC) || main()

使用 scc 命令行工具编译:

scc main.jl -o main --bundle --verbose
...
[setup julia] running
[setup julia] finished
[NIR method table & Pkg.jl] initializing
[NIR method table & Pkg.jl] initialized
[julia precompilation] running
evaluating main.jl...
[julia precompilation] finished
building apps...
[codegen] 1: typeof(Base._string_n)
[codegen] 2: Type{InexactError}
[codegen] 3: typeof(Core.throw_inexacterror)
...
...
...
[codegen] 279: Type{String}
[codegen] 280: typeof(print)
[codegen] 281: typeof(println)
[codegen] 282: typeof(display)
...

--verbose 编译选项开启情况下,编译期间信息会逐步输出,以便确认代码生成是否正常进行。如果在编译中阻塞或报错,编译信息有助于定位排查问题。

# 通过 --no-gc 排查垃圾回收兼容性问题

代码生成工具使用垃圾回收库 bdwgc 进行内存管理,该库功能强大支持绝大部分应用场景,并已经在多种硬件和软件平台上得到验证,但是还是无法避免一些特殊场景下的兼容性问题, 如协程 (opens new window)的使用。对于此类场景,代码生成工具提供 --no-gc 选项,禁用垃圾回收, 使用代码生成工具提供的自定义内存管理器管理内存,该管理器会在导出入口调用结束时统一回收内存,避免对垃圾回收库的依赖。

    Flags:
        ...
        --no-gc     : Disable garbage collection, use custom memory management.

如需使用该功能,仅需在原有的 scc 编译命令中添加 --no-gc 选项即可。

# 高级实验性功能:严格类型稳定性检查

默认情况下,Syslab 代码生成不在编译期间对动态调用报错,只在程序实际运行到动态调用的分支时、抛出运行时错误。

针对熟悉 Julia 类型稳定、且熟悉 Julia 生态类型稳定情况的高级用户,我们提供 --full-aot ALL 选项。

此选项支持在编译期检查动态调用,将其视为编译错误。

    Options:
        ...
        --full-aot  : Specify the modules where dynamic calls are treated as compile errors.
                      Currently, the only valid value is 'ALL'.

未来版本的 Syslab 代码生成工具将支持对指定模块进行严格类型稳定性检查,方便高级用户排查指定模块的类型稳定问题。