我在 Julia 中遇到一个问题,调用存储在结构中的函数会导致意外的内存分配,如下面的代码所示。直接函数调用(变体 1)不会导致任何分配,但函数存储在结构体或变量中的其他变体会导致 16 字节的分配。这会显着影响性能。为什么会发生这种情况,如何避免这些分配?
using BenchmarkTools
mutable struct Foo
v::Float64
f::Function
end
function add_one(x::Foo)
x.v += 1
end
x = Foo(0.0, add_one)
println("variant 1")
@btime add_one(x)
@btime add_one(x)
println("variant 2")
@btime x.f(x)
@btime x.f(x)
println("variant 3")
func = x.f
@btime func(x)
@btime func(x)
println("variant 4")
func1 = add_one
@btime func1(x)
@btime func1(x)
println("variant 5")
func2 = (x::Foo) -> begin x.v += 1 end
@btime func2(x)
@btime func2(x)
println("func = '$func'")
输出:
variant 1
6.900 ns (0 allocations: 0 bytes)
6.900 ns (0 allocations: 0 bytes)
variant 2
61.060 ns (1 allocation: 16 bytes)
61.122 ns (1 allocation: 16 bytes)
variant 3
28.414 ns (1 allocation: 16 bytes)
28.543 ns (1 allocation: 16 bytes)
variant 4
28.442 ns (1 allocation: 16 bytes)
28.342 ns (1 allocation: 16 bytes)
variant 5
27.711 ns (1 allocation: 16 bytes)
27.739 ns (1 allocation: 16 bytes)
func = 'add_one'
这个问题是 Julia 设计固有的吗?为什么变体 2 明显慢于变体 3-5?
我打算广泛使用变体 2 和仅涉及几个操作的简单函数,这意味着我不能忽略开销。
你需要避免抽象类型:
mutable struct Foo2{T <: Function}
v::Float64
f::T
end
function add_one(x::Foo2)
x.v += 1.0 # use the same type of one as the data or use `one(x.v)`
end
现在你可以做:
julia> y = Foo2(0.0, add_one)
Foo2{typeof(add_one)}(0.0, add_one)
julia> @btime $y.f($y)
2.500 ns (0 allocations: 0 bytes)
500503.0
请注意,您需要在测试中插入
y
- 否则您正在测量 Julia 获得 y
类型的努力。
您还可以将其设为
const
(从而使基准测试的类型稳定):
julia> const z = Foo2(0.0, add_one);
julia> @btime z.f(z)
2.500 ns (0 allocations: 0 bytes)
500503.0
最后但并非最不重要的一点是
@btime
只能运行一次。