使用Symbolics创建的Julia函数编译耗时过长的原因与优化
问题:Symbolics生成函数首次调用编译耗时过长的原因与解决方法?
我是Julia的新手,可能忽略了一些基础问题。我使用Symbolics包创建函数,代码如下:
_, f = build_function(F, x) write("function.jl", string(f)) myf = include("function.jl")
其中F是符号多项式向量值函数,x是符号向量。首次调用myf时,Julia耗时极长(可达1小时)才返回结果,但后续调用仅需纳秒级时长。使用precompile时也需要同样久的时间,因此我认为是编译器编译myf耗时过长。请问这是什么原因?有什么缓解方法吗?
附:以下是function.jl的部分内容,供参考
function (ˍ₋out, ˍ₋arg1) begin begin @inbounds begin ˍ₋out[1] = 1.0 [...] ˍ₋out[48] = ˍ₋arg1[47] ˍ₋out[49] = (+)(-0.7071067811865475, (*)(0.7071067811865475, (^)(ˍ₋arg1[1], 2))) ˍ₋out[50] = (*)(ˍ₋arg1[1], ˍ₋arg1[2]) ˍ₋out[51] = (*)(ˍ₋arg1[1], ˍ₋arg1[3]) [...] ˍ₋out[74308] = (*)((*)((*)(ˍ₋arg1[36], ˍ₋arg1[4]), ˍ₋arg1[47]), ˍ₋arg1[6]) ˍ₋out[74309] = (*)((*)((+)(-0.7071067811865475, (*)(0.7071067811865475, (^)(ˍ₋arg1[37], 2))), ˍ₋arg1[4]), ˍ₋arg1[6]) ˍ₋out[74310] = (*)((*)((*)(ˍ₋arg1[37], ˍ₋arg1[38]), ˍ₋arg1[4]), ˍ₋arg1[6]) [...] nothing end end end end
原因分析
- 函数规模过大:从示例能看到输出向量包含7万多个元素,每个元素对应独立的表达式,编译器需要逐一完成类型推断、代码优化、机器码生成等步骤,单次编译的计算量极大,自然耗时久。
- 代码生成风格问题:Symbolics生成的代码用了
(+)、(*)这类前缀调用形式,而非常规的中缀运算符,这会增加编译器需要处理的函数调用节点数量,进一步拉长编译时间。 - JIT编译特性:Julia是即时编译语言,首次执行函数时才会完成全部编译流程,这类巨型函数的编译开销远超普通函数;后续调用直接复用已生成的机器码,所以速度极快。
缓解方法
- 拆分巨型函数:把输出向量分成若干个小块,每个小块对应一个子函数,让编译器分批处理,减少单次编译的压力,同时也能利用编译的并行性。
- 优化代码生成参数:调用
build_function时,指定expression=Val{false}生成更紧凑的代码,或者选择target=:llvm等更贴近底层的目标,减少编译器的优化负担;也可以添加@fastmath标记让生成的代码更简洁高效。 - 预编译系统镜像:用
PackageCompiler将函数和依赖的包一起编译成系统镜像,启动Julia后直接加载镜像,跳过即时编译步骤,彻底解决首次调用慢的问题。 - 避免文件IO环节:直接使用
build_function返回的函数对象,无需写入文件再include,减少额外的代码解析和IO开销,提升编译效率。 - 使用生成函数:借助Julia的
@generated宏,将部分计算逻辑转移到编译阶段,优化编译流程,降低运行时的编译压力。
内容的提问来源于stack exchange,提问作者CaG
相关产品推荐
相关产品推荐

