尝试整合Julia代码的优化方法
不应该优化所有代码
Profiling
只需要测代码中较慢的函数,一般使用BenchmarkTools.jl
中的宏,在硬件相同的情况下测试…
代码优化的过程往往牺牲了其他方面:
- 优化后的代码通常更长更难懂,可读性较差
- 难以适应未来改动,难以测试或移植
单纯地让电脑运行更久可以有效抵消优化代码的时间
元编程/代码简写/生成模式
macro
是在编译时运行并输出代码的函数,可以构建Julia
表达式
macro defName(name, definition)
return quote
macro $(esc(name))()
esc($(Expr(:quote, definition)))
end
end
end
@defName myField begin
a::Int
end
struct test
@myField
b::Int
end
Expr(:quote, a = 1)
就相当于 :(:(a = 1))
eval
在全局作用域中运行,@eval
相当于eval(quote ... end)
的简写
向量化的代码会创建不必要的中间数组,产生不必要的“临时分配”,减慢Julia
的速度
使用广播.
符号,看上去像向量化代码,同时进行一个循环的展开:
x .= x .+ f.(x)
等价于
for i in eachindex(x)
x[i] = x[i] + f(x[i])
end
- 任何性能关键或正在被基准测试的代码都应该放在函数内部
- 重新排列结构体的字段可以减少填充(Julia 使用与 C 相同的结构体布局)
- 结构体形成的数组(AOS)和包含数组的结构体(SOA)内存布局不一样,使用AOS可能便于理解维护,使用SOA防止内存填充,有利于SIMD的使用…结合一下AOSOA
- 避免在容器和结构体中使用抽象类型
计算自由度/一些Julia知识
写个完全不具备代表性的例子,大概是典型的嵌套for循环,$O(n^2)$
function nearest_neighbour(points)
N, D = size(points)
neighbours = zeros(Int, N)
for i in 1:N
point_i = points[:, i]
min_squared_dist = typemax(eltype(points))
for j in 1:N
point_j = points[:, j]
squared_dist = sum((point_i .- point_j).^2)
if min_squared_dist < squared_dist
min_squarerd_dist = squared_dist
neighbours[i] = j
end
end
end
return neighbours
end
稍微分析一下以避免使用拉跨算法,或者做了不必要的操作…
Array
的迭代顺序:Julia 列主序(矩阵),A[i, j]
应该尽量固定i
而操作j
allocations
一般指堆分配,由GC收集,应该尽量减少(提前预分配/避免无意义的拷贝)- 使用
StaticArrays.jl
把小数组塞到栈上
Amdahl’s Law:估计固定工作量的代码并行加速程度
$$ S(s) = \frac{1}{(1-p) + \frac{p}{s}} $$其中$p$为从改进的$s$资源中受益的时间比例
Gustafson’s Law:估计增加工作量的并行加速程度
$$ S = \frac{T_s}{T_p} = (1 - f) + fN $$
说些什么吧!