我打算出个视频分享我提升计算速度的经验,遇到点问题和寻求建议

我打算从下面这个代码开始(我的学科相空间的演化过程),

restenergy = 0.511e6            # 电子静止质量[eV]

nturns = 100000
v1 = 3.6395053705870048e+06
r = 0.1801760487622639
ϕs = 2.1575815005097385
ϕ2s = 6.0620746573056303
h1 = 756
h = 3
circum = 1360.4
centerenergy = 6e9
αc = 0.0000156


@time begin
    zvecs = []; δvecs = [] # 这两个用来保存相空间演化的历史数据

    for i = 1:nturns
        δvec .= δvec .+ v1 .* (sin.(ϕs .- 2 * h1 * π .* zvec ./ circum) .- sin(ϕs) .+ r .* sin.(ϕ2s .- 2 * h1 * h * π .* zvec ./ circum) .- r * sin(ϕ2s)) ./ centerenergy ./ circum
        ηvec = αc .- 1 ./ (centerenergy .* (1 .+ δvec) ./ restenergy).^2
        zvec .= zvec .- ηvec .* δvec
        push!(zvecs, zvec)
        push!(δvecs, δvec)
    end
end

分三个视频快速讲讲大致怎么提高计算性能:

  1. 针对IO的优化(上面这种算I/O密集型吧)
    1.1 数组内存预分配——用zeros()去提前分配zvec,δvec,ηvec内存
    1.2 @.代替.+等等——这里出现了问题
    1.3 底层代码标量优先——改成for循环形式,但是行在外列在内;
    1.4 第一个index最快——改成行在内
  2. 针对编译的优化(上面的代码稍微修改,不保存历史数据,只导出最后结果,算CPU密集型吧)
    2.1 包装成函数——看看函数编译的提升效果
    2.2 编写类型稳定的函数——δvecVector{Int64}类型,算着算着,变Vector{Float64}类型
    2.3 分离核心函数——循环调用的部分提取成函数
    2.4 ——for循环加@inbounds,@simd
    2.5 全局变量的常数声明——之前的函数编译时,全局变量电子静止质量restenergy没有指定为常数。加const期望看到速度提高
  3. 针对CPU的优化
    3.1 多线程——Threads.@spawn,@sync的使用

上面这个我选的例题和这个目录安排,各位有什么建议吗?


再来说说遇到的问题。下面这个代码,用@.但是速度比上面慢。

@time begin
    zvecs = []
    δvecs = []

    for i = 1:nturns
        @. δvec = δvec + v1 * (sin(ϕs - 2 * h1 * π * zvec / circum) - sin(ϕs) + r * sin(ϕ2s - 2 * h1 * h * π * zvec / circum) - r * sin(ϕ2s)) / centerenergy / circum
        ηvec = @. αc - 1 / (centerenergy * (1 + δvec) / restenergy)^2
        @. zvec = zvec - ηvec * δvec
        push!(zvecs, zvec)
        push!(δvecs, δvec)
    end
end

结果:43.634120 seconds (6.20 M allocations: 7.810 GiB, 1.48% gc time)
而上面: 31.876649 seconds (7.19 M allocations: 7.807 GiB, 2.32% gc time, 1.09% compilation time)
我猜测,这个例子,@.没有带来好的效果,是由于.+,.*编译了引起的。但是我没有包装到函数里,怎么会编译呢?

我在尝试跑你贴的代码:

ERROR: LoadError: UndefVarError: zvec not defined
Stacktrace:
 [1] macro expansion
   @ /home/local/hongyang/Documents/julia/tmp/test.jl:19 [inlined]

zvec在刚进循环的时候没有定义,δvec也没有定义。

抱歉,之前其实我有生成数据。

using BSON: @save

zvec = randn(10000)*0.12
δvec = randn(10000)*1.06e-3

@save "zvec_δvec.bson" zvec δvec

后面的代码依次贴出。

1.0 初始代码

using BSON: @load
using BenchmarkTools

@load "zvec_δvec.bson" zvec δvec

restenergy = 0.511e6            # [eV]

nturns = 10001

v1 = 3.6395053705870048e+06
r = 0.1801760487622639
ϕs = 2.1575815005097385
ϕ2s = 6.0620746573056303
h1 = 756
h = 3
circum = 1360.4
centerenergy = 6e9
αc = 0.0000156


@benchmark begin
    npart = size(zvec, 1)
    zvecs = []
    δvecs = []

    for i = 1:nturns
        δvec .= δvec .+ v1 .* (sin.(ϕs .- 2 * h1 * π .* zvec ./ circum) .- sin(ϕs) .+ r .* sin.(ϕ2s .- 2 * h1 * h * π .* zvec ./ circum) .- r * sin(ϕ2s)) ./ centerenergy ./ circum
        ηvec = αc .- 1 ./ (centerenergy .* (1 .+ δvec) ./ restenergy) .^ 2
        zvec .= zvec .- ηvec .* δvec
        push!(zvecs, zvec)
        push!(δvecs, δvec)
    end
end

结果:

BenchmarkTools.Trial: 2 samples with 1 evaluation.
 Range (min … max):  3.001 s …   3.001 s  ┊ GC (min … max): 0.78% … 0.97%
 Time  (median):     3.001 s              ┊ GC (median):    0.88%
 Time  (mean ± σ):   3.001 s ± 50.346 μs  ┊ GC (mean ± σ):  0.88% ± 0.13%

  █                                                       █
  █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ ▁
  3 s            Histogram: frequency by time           3 s <

 Memory estimate: 794.62 MiB, allocs estimate: 639062.

1.1 数组内存预分配

using BSON: @load
using BenchmarkTools

@load "zvec_δvec.bson" zvec δvec

restenergy = 0.511e6            # [eV]

nturns = 10001

v1 = 3.6395053705870048e+06
r = 0.1801760487622639
ϕs = 2.1575815005097385
ϕ2s = 6.0620746573056303
h1 = 756
h = 3
circum = 1360.4
centerenergy = 6e9
αc = 0.0000156


@benchmark begin
    npart = size(zvec, 1)
    zvecs = zeros(nturns, npart)
    δvecs = zeros(nturns, npart)
    ηvec = zeros(size(zvec))

    for i = 1:nturns
        δvec .= δvec .+ v1 .* (sin.(ϕs .- 2 * h1 * π .* zvec ./ circum) .- sin(ϕs) .+ r .* sin.(ϕ2s .- 2 * h1 * h * π .* zvec ./ circum) .- r * sin(ϕ2s)) ./ centerenergy ./ circum
        ηvec = αc .- 1 ./ (centerenergy .* (1 .+ δvec) ./ restenergy) .^ 2
        zvec .= zvec .- ηvec .* δvec
        zvecs[i, :] .= zvec
        δvecs[i, :] .= δvec
    end
end

结果:

BenchmarkTools.Trial: 1 sample with 1 evaluation.        
 Single result which took 5.851 s (1.51% GC) to evaluate,
 with a memory estimate of 2.27 GiB, over 777021 allocations.

1.2 使用$@.$代替点语法

using BSON: @load
using BenchmarkTools

@load "zvec_δvec.bson" zvec δvec

restenergy = 0.511e6            # [eV]

nturns = 10001

v1 = 3.6395053705870048e+06
r = 0.1801760487622639
ϕs = 2.1575815005097385
ϕ2s = 6.0620746573056303
h1 = 756
h = 3
circum = 1360.4
centerenergy = 6e9
αc = 0.0000156


@benchmark begin
    npart = size(zvec, 1)
    zvecs = zeros(nturns, npart)
    δvecs = zeros(nturns, npart)
    ηvec = zeros(size(zvec))

    for i = 1:nturns
        @. δvec = δvec + v1 * (sin(ϕs - 2 * h1 * π * zvec / circum) - sin(ϕs) + r * sin(ϕ2s - 2 * h1 * h * π * zvec / circum) - r * sin(ϕ2s)) / centerenergy / circum
        @. ηvec = αc - 1 / (centerenergy * (1 + δvec) / restenergy)^2
        @. zvec = zvec - ηvec * δvec
        zvecs[i, :] .= zvec
        δvecs[i, :] .= δvec
    end
end

结果:

BenchmarkTools.Trial: 1 sample with 1 evaluation.
 Single result which took 6.913 s (0.02% GC) to evaluate,
 with a memory estimate of 1.53 GiB, over 737017 allocations.

我已经看不懂了。
1.0 初始我认为的垃圾代码: 3.001s, 794.62MiB
1.1 数组内存预分配 ——5.851s, 2.27 GiB
1.2 @.代替.+等等 ——6.913s, 1.53 GiB
可能,push!是按照内存顺序存的,速度快些;我虽说是预分配了数组,但是强行不按内存顺序去计算,速度慢了些?

我观察到以下现象(用的SysLab):

  • 刚刚打开程序后,首次运行.jl文件,会报编译;
  • 之后哪怕修改了程序内容,再运行,也不会报编译。

因此,我之前以为只有函数(包括导入库的函数)会编译,是个错误。

任何对性能至关重要的代码都应该在函数内部。 由于 Julia 编译器的工作方式,函数内部的代码往往比顶层代码运行得更快。

这并不是说我直接写外面的for循环就不编译了。
另外对第二点表示不太理解,记得VS Code中,每次执行启动都很慢。下面是输出

  3.847231 seconds (2.85 M allocations: 912.396 MiB, 3.16% gc time, 17.80% compilation time)

┌ Warning: Assignment to `ηvec` in soft scope is ambiguous because a global variable by the same name exists: `ηvec` will be treated as a new local. Disambiguate by using `local ηvec` to suppress this warning or `global ηvec` to assign to the existing global variable.
└ @ d:\Documents\SysLabDocument\P3.数组内存预分配.jl:29
  6.027418 seconds (1.06 M allocations: 2.285 GiB, 2.05% gc time, 1.75% compilation time)

  7.936978 seconds (2.33 M allocations: 1.610 GiB, 7.21% gc time, 6.39% compilation time)

┌ Warning: Assignment to `ηvec` in soft scope is ambiguous because a global variable by the same name exists: `ηvec` will be treated as a new local. Disambiguate by using `local ηvec` to suppress this warning or `global ηvec` to assign to the existing global variable.
└ @ d:\Documents\SysLabDocument\P4.使用@.替代.jl:29
  7.982928 seconds (757.02 k allocations: 2.275 GiB, 8.49% gc time)

  2.632680 seconds (166.96 k allocations: 1.494 GiB, 0.12% gc time)

前三次是我刚打开SysLab,立马逐个运行我上面讲的三个文档(三个代码);后面两个是我随便更改代码内容的输出结果

这里每个版本之间变化的量有点多,并不是完全意义上控制变量的实验;但我感觉最主要的问题是,那些影响代码优化的不同部分的重要等级是不一样的,所以比如当类型不稳定这个最重要的部分没有解决时,其他的改动都是次要的。

理论上@.和逐个点操作生成底层代码应该是一样的,如果不一样,要么是原本的两个表达式所做的事情不同(大概率),要么是宏编程更改语句时宏本身的bug(小概率)。

建议当你对比行优先和列优先之前,首先展示类型稳定的优化。

1 个赞

IO 一般指硬盘、网络吧。
你这个明显计算密集啊

我觉得算是I/O密集型计算吧?CPU这里不占用多少时间,第二大类会改改,让CPU多用用,少往内存读写

@. 默认会让所有调用变成广播。这会导致sin(ϕs)之类的计算归入到广播的循环内部。考虑到你没申明const.我想编译器在这里不会尝试常量计算。而目前Julia端的effect还没集成到codegen里, 由于sin默认是不内联的LLVM也无法利用LICM避免重复计算。

1 个赞

可以把理论公式贴出来嘛?还有视频地址?