julia 中内建函数与嵌套循环的效率

Introduction

前面做宽动态成像(前面的文章)时遇到了运行瓶颈,主要是在计算量上,要计算的式子如下:

\[\begin{eqnarray} I_{out}(x,y)=\sum_{i=1}^{n}\sum_{j=1}^{m}B_{ij}(x,y)U(x-rx_{ij},y-ry_{ij})I_{d_{ij}}(x,y) \end{eqnarray} \]

而Bij(x,y)的计算也有求和运算:

\[\begin{eqnarray} B_{ij}(x,y)=\frac{e^{- \left( \frac{ (x-rx_{ij})^2 }{2\sigma^2_x} + \frac{ (y-ry_{ij})^2 }{2\sigma^2_y} \right) }} {\sum_{p=1}^{m}\sum_{q=1}^{n}e^{- \left( \frac{ (x-rx_{pq})^2 }{2\sigma^2_x} + \frac{ (y-ry_{pq})^2 }{2\sigma^2_y} \right) } } \end{eqnarray} \]

做实验的时候设置m=20,n=15,H,W分别为597,397,结果一循环就回不来了。。。(对julia的循环优化也是充满信心)

后面试了下用空间换效率的办法:增加一维存储m×n个的矩阵,然后用内置求和,发现提升明显。。。

...
BM=zeros(H,W,M*N)*0.
X=collect(1:W)'.-zeros(H)
Y=collect(1:H).-zeros(W)'

for pn=1:N
    for pm=1:M
        BM[:,:,pn+(pm-1)*N]=1./exp( (X-MNCenMatrix[pm,pn,1]).^2/(2*sigmaX^2) + (Y-MNCenMatrix[pm,pn,2]).^2/(2*sigmaY^2) ) .*
                                     convert(Array{Int64,2},epsilon.>map(max,abs(X-MNCenMatrix[pm,pn,1]),abs(Y-MNCenMatrix[pm,pn,2])))
    end
end
BM=BM./sum(BM,3)

for pn=1:N
    for pm=1:M
        retI[:]=retI[:]+(BM[:,:,pn+(pm-1)*N].*ILM[:,:,pn+(pm-1)*N])[:]
    end
end
...

Followup

issue 1: 与内建函数的差距

然后就想看看内建函数和嵌套函数差了多远:

H,W=900,800
C=300
m=rand(H,W,C)
n=rand(H,W)

function matrixSum!(m::Array{Float64,3},ret::Array{Float64,2})
H,W,C=size(m)
for pw=1:W
    for ph=1:H
        for pc=1:C
            ret[ph,pw]=ret[ph,pw]+m[ph,pw,pc]
        end
    end
end
end

@time n1=sum(m,3)
@time matrixSum!(m,n)

返回:

# i3 CPU 530 @ 2.93GHz × 4 
0.687958 seconds (19 allocations: 5.494 MB)
2.001088 seconds (6.04 k allocations: 246.260 KB)

对这结果甚是无语。。。分配了5.5MB也比你跑得快,是分配次数多限制了提升?难道这样的嵌套写法还可以大幅优化?(我一直以为这就是内存最简写法了)

issue 2: 内存访问顺序

突然想到访问次序有些关键,于是调整了下:

H,W=900,800
C=300
m=rand(H,W,C)
n=rand(H,W)
function matrixSum!(m::Array{Float64,3},ret::Array{Float64,2})
H,W,C=size(m)

for pc=1:C
    for pw=1:W
        for ph=1:H
            ret[ph,pw]=ret[ph,pw]+m[ph,pw,pc]
        end
    end
end
end

@time n1=sum(m,3)
@time matrixSum!(m,n)

返回:

# 首次运行
0.879469 seconds (193.28 k allocations: 13.598 MB)
0.787520 seconds (6.47 k allocations: 269.166 KB)

# 再次运行
0.609809 seconds (19 allocations: 5.494 MB)
0.788194 seconds (6.04 k allocations: 246.260 KB)

就内建函数来看,内存分配的次数并没有太多的决定时间消耗;
就内建函数与循环嵌套对比来看,刚才是访问顺序的问题。
那么内存为什么分配了那么多次呢?

issue 3: 内存分配次数

突然想到julia的动态编译机制,于是拿出来单独运行了次:

julia> @time matrixSum!(m,n)
  0.765424 seconds (4 allocations: 160 bytes)

看来是这样的。

Conclusion

  1. julia 的优化确实做得不错,几乎达到了极致;
  2. 访问顺序很关键;
  3. 分配次数并不一定会是瓶颈。
posted @ 2017-05-02 16:12  rotxin  阅读(782)  评论(0编辑  收藏  举报