Julia ---- 广播变量的性能问题

翻译内容

在Julia中广播是一种编写向量化代码(类似Matlab)的方法,它是一种高效和明确的方法。除了代码执行速度更快以外,显式矢量化也是一个显著的优点。但也是有限制的,对复杂函数进行矢量化可能需要相当多的练习才能熟练使用,但通常可读性较差。

Julia中使用点广播(矢量化)。如果希望调用函数时使用向量(对向量的每个元素应用相同的函数),只需在函数调用上加一个点。例如,值向量的正弦变成sin.[1.1,0.3,2.3]);注意sin和第一个括号之间的额外点。

在Julia v0.7/1.0中,广播的工作方式发生了一些变化。(请参阅Extensible broadcast fusion。)它现在创建一系列Broadcasted对象,这些对象融合在一起,然后最终实现函数以给出最终结果。例如,

r = sqrt(sum(x.^2 .+ y.^2))

在内部,它被重写(“lowered”)为

r = sqrt(sum(materialize(broadcasted(+, broadcasted(^, x, 2), broadcasted(^, y, 2)))))

(这在细节上不太准确,因为平方的实现稍有不同。)请注意广播调用的代码层次结构,它们封装在要具体化的调用中的。这就是广播融合的魔力所在,这也使Julia能够构造性能良好的代码。广播调用创建一组嵌套的广播对象,这些对象包含(延迟计算的)矢量表达式,实现的调用就是由此创建最终矢量。

大多数时候,我们想要这种自动魔法实现,但有时候不需要。

考虑上面sum函数的情况;将在内存中为计算x.^2+y.^2分配一个向量,如果x和y很大,则将不必要地为中间值分配大量内存。既然sum函数不会同时使用所有的值,我们就不能懒洋洋地把x.^2+y.^2作为一个个单独的数字来计算,然后一个一个地把它们输入sum函数吗?例如,可以这么做

    const   acc = 0.0
    for i = eachindex(x, y)
        acc += x[i]^2 + y[i]^2
    end
    r = sqrt(acc)

在这种情况下,使用显式for循环来试图避免的上面提到的分配大量内容的情况(这里有一个问题,我们为什么要费心使用广播?)。我们是否可以从广播中提取延迟表示,而不具体化中间结果吗?

答案是肯定的,但不幸的是,这还不是Julia base库的一部分。下面的代码为我们提供了一个惰性宏,它使我们能够访问广播创建的延迟加载的表达式,并在上下文中其他的代码里显式地使用它。

@inline _lazy(x) = x[1]  # unwrap the tuple
@inline Broadcast.broadcasted(::typeof(_lazy), x) = (x,)  # 使用tuple包装广播对象来避免实时具体化,以达到延迟的效果
macro lazy(x)
    return esc(:(_lazy(_lazy.($x))))
end

现在我们可以比较延迟加载的版本和实时具体化版本。

julia> using BenchmarkTools

julia> x = rand(1_000_000) ; y = rand(1_000_000) ;

julia> @btime sqrt(sum(x.^2 .+ y.^2))  # normal eager evaluation
  2.837 ms (16 allocations: 7.63 MiB)
816.7514405417339

julia> @btime sqrt(sum(@lazy x.^2 .+ y.^2))  # lazy broadcasted evaluation
  1.075 ms (12 allocations: 208 bytes)
816.7514405417412

注意内存消耗:正常版本为7.63mib,而延迟计算的版本为208字节。类似地,延迟计算版本的速度要快得多(尽管这在很大程度上取决于使用的向量的大小)。在这两种情况下有一个稍微不同的结果,因为Julia sum函数对向量和迭代器使用稍微不同的算法。

为什么延迟计算的版本不是默认版本呢?这里有一个警告:一旦你做了延迟计算的评估,性能就变得更加依赖于具体的问题了,使用延迟计算可以变得更快(在本例中),但同样,在其他地方,它也可以变得更慢。这时候BenchmarkTools.jl是您的朋友了,你可以使用它调试具体的性能!

本地验证了下:

使用版本是Julia-1.0.5

using BenchmarkTools

@inline _lazy(x) = x[1] # unwrap the tuple
@inline Broadcast.broadcasted(::typeof(_lazy), x) = (x,) # 使用tuple包装广播对象来避免实时具体化,以达到延迟的效果
macro lazy(x)
    return esc(:(_lazy(_lazy.($x))))
end

x = rand(1_000_000) ; y = rand(1_000_000) ;

@btime sqrt(sum(x.^2 .+ y.^2)) # 正常的实时计算
#816.3678796598351

@btime sqrt(sum(@lazy x.^2 .+ y.^2)) # 使用延迟计算
#816.3678796598531


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章