闭包列表的类型稳定性
Type stability for lists of closures
我正在尝试在 Julia 中设计一些代码,这些代码将采用用户提供的函数列表,并对它们应用一些代数运算。
如果这些函数是闭包,则似乎不会推断出此函数列表的 return 值,根据@code_warntype.
会导致类型不稳定的代码
我尝试为闭包提供 return 类型,但似乎无法找到正确的语法。
这是一个例子:
functions = Function[x -> x]
function f(u)
ret = zeros(eltype(u), length(u))
for func in functions
ret .+= func(u)
end
ret
end
运行 这个:
u0 = [1.0, 2.0, 3.0]
@code_warntype f(u0)
并获得
Body::Array{Float64,1}
1 ─ %1 = (Base.arraylen)(u)::Int64
│ %2 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Float64,1}, svec(Any, Int64), :(:ccall), 2, Array{Float64,1}, :(%1), :(%1)))::Array{Float64,1}
│ %3 = invoke Base.fill!(%2::Array{Float64,1}, 0.0::Float64)::Array{Float64,1}
│ %4 = Main.functions::Any
│ %5 = (Base.iterate)(%4)::Any
│ %6 = (%5 === nothing)::Bool
│ %7 = (Base.not_int)(%6)::Bool
└── goto #4 if not %7
2 ┄ %9 = φ (#1 => %5, #3 => %15)::Any
│ %10 = (Core.getfield)(%9, 1)::Any
│ %11 = (Core.getfield)(%9, 2)::Any
│ %12 = (%10)(u)::Any
│ %13 = (Base.broadcasted)(Main.:+, %3, %12)::Any
│ (Base.materialize!)(%3, %13)
│ %15 = (Base.iterate)(%4, %11)::Any
│ %16 = (%15 === nothing)::Bool
│ %17 = (Base.not_int)(%16)::Bool
└── goto #4 if not %17
3 ─ goto #2
4 ┄ return %3
那么,如何使这种代码类型稳定?
您的代码中存在多个层次问题(不幸的是类型稳定性):
functions
是一个全局变量,所以从根本上说你的代码不会是稳定的
- 即使您将
functions
移入函数定义并且它将是一个向量,代码仍然是类型不稳定的,因为容器将具有抽象的 eltype(即使您删除了 Function
前缀在 [
之前,如果你有不止一种不同的功能)
- 如果您将向量更改为元组(那么集合
functions
的类型将是稳定的),该函数仍将是类型不稳定的,因为您使用的循环无法在内部推断 return 类型 func(u)
解决方案是使用 @generated
函数将循环展开为 func(u)
的一系列连续应用程序 - 那么您的代码将是稳定的类型。
但是,总的来说,我认为,假设 func(u)
是一项昂贵的操作,那么代码中的类型不稳定性应该不会有太大问题,因为最后您将 return 的值转换为func(u)
到 Float64
无论如何。
编辑 一个 @generated
版本,用于与 Tim Holy 提出的内容进行比较。
@generated function fgenerated(u, functions::Tuple{Vararg{Function}})
expr = :(ret = zeros(eltype(u), size(u)))
for fun in functions.parameters
expr = :($expr; ret .+= $(fun.instance)(u))
end
return expr
end
如果你想要任意函数的类型稳定性,你必须将它们作为元组传递,这允许 julia 提前知道哪个函数将在哪个阶段应用。
function fsequential(u, fs::Fs) where Fs<:Tuple
ret = similar(u)
fill!(ret, 0)
return fsequential!(ret, u, fs...)
end
@inline function fsequential!(ret, u, f::F, fs...) where F
ret .+= f(u)
return fsequential!(ret, u, fs...)
end
fsequential!(ret, u) = ret
julia> u0 = [1.0, 2.0, 3.0]
3-element Array{Float64,1}:
1.0
2.0
3.0
julia> fsequential(u0, (identity, x-> x .+ 1))
3-element Array{Float64,1}:
3.0
5.0
7.0
如果您使用 @code_warntype
检查它,您会发现它是可推断的。
fsequential!
是有时称为 "lispy tuple programming" 的示例,在该示例中,您一次迭代处理一个参数,直到所有可变参数参数都用完。这是一个强大的范例,比使用数组的 for
循环允许更灵活的推理(因为它允许 Julia 为每个 "loop iteration" 编译单独的代码)。然而,它通常只有在容器中的元素数量相当少时才有用,否则你最终会得到非常长的编译时间。
类型参数 F
和 Fs
看起来没有必要,但它们旨在强制 Julia 为您传入的特定函数专门化代码。
我正在尝试在 Julia 中设计一些代码,这些代码将采用用户提供的函数列表,并对它们应用一些代数运算。
如果这些函数是闭包,则似乎不会推断出此函数列表的 return 值,根据@code_warntype.
会导致类型不稳定的代码我尝试为闭包提供 return 类型,但似乎无法找到正确的语法。
这是一个例子:
functions = Function[x -> x]
function f(u)
ret = zeros(eltype(u), length(u))
for func in functions
ret .+= func(u)
end
ret
end
运行 这个:
u0 = [1.0, 2.0, 3.0]
@code_warntype f(u0)
并获得
Body::Array{Float64,1}
1 ─ %1 = (Base.arraylen)(u)::Int64
│ %2 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Float64,1}, svec(Any, Int64), :(:ccall), 2, Array{Float64,1}, :(%1), :(%1)))::Array{Float64,1}
│ %3 = invoke Base.fill!(%2::Array{Float64,1}, 0.0::Float64)::Array{Float64,1}
│ %4 = Main.functions::Any
│ %5 = (Base.iterate)(%4)::Any
│ %6 = (%5 === nothing)::Bool
│ %7 = (Base.not_int)(%6)::Bool
└── goto #4 if not %7
2 ┄ %9 = φ (#1 => %5, #3 => %15)::Any
│ %10 = (Core.getfield)(%9, 1)::Any
│ %11 = (Core.getfield)(%9, 2)::Any
│ %12 = (%10)(u)::Any
│ %13 = (Base.broadcasted)(Main.:+, %3, %12)::Any
│ (Base.materialize!)(%3, %13)
│ %15 = (Base.iterate)(%4, %11)::Any
│ %16 = (%15 === nothing)::Bool
│ %17 = (Base.not_int)(%16)::Bool
└── goto #4 if not %17
3 ─ goto #2
4 ┄ return %3
那么,如何使这种代码类型稳定?
您的代码中存在多个层次问题(不幸的是类型稳定性):
functions
是一个全局变量,所以从根本上说你的代码不会是稳定的- 即使您将
functions
移入函数定义并且它将是一个向量,代码仍然是类型不稳定的,因为容器将具有抽象的 eltype(即使您删除了Function
前缀在[
之前,如果你有不止一种不同的功能) - 如果您将向量更改为元组(那么集合
functions
的类型将是稳定的),该函数仍将是类型不稳定的,因为您使用的循环无法在内部推断 return 类型func(u)
解决方案是使用 @generated
函数将循环展开为 func(u)
的一系列连续应用程序 - 那么您的代码将是稳定的类型。
但是,总的来说,我认为,假设 func(u)
是一项昂贵的操作,那么代码中的类型不稳定性应该不会有太大问题,因为最后您将 return 的值转换为func(u)
到 Float64
无论如何。
编辑 一个 @generated
版本,用于与 Tim Holy 提出的内容进行比较。
@generated function fgenerated(u, functions::Tuple{Vararg{Function}})
expr = :(ret = zeros(eltype(u), size(u)))
for fun in functions.parameters
expr = :($expr; ret .+= $(fun.instance)(u))
end
return expr
end
如果你想要任意函数的类型稳定性,你必须将它们作为元组传递,这允许 julia 提前知道哪个函数将在哪个阶段应用。
function fsequential(u, fs::Fs) where Fs<:Tuple
ret = similar(u)
fill!(ret, 0)
return fsequential!(ret, u, fs...)
end
@inline function fsequential!(ret, u, f::F, fs...) where F
ret .+= f(u)
return fsequential!(ret, u, fs...)
end
fsequential!(ret, u) = ret
julia> u0 = [1.0, 2.0, 3.0]
3-element Array{Float64,1}:
1.0
2.0
3.0
julia> fsequential(u0, (identity, x-> x .+ 1))
3-element Array{Float64,1}:
3.0
5.0
7.0
如果您使用 @code_warntype
检查它,您会发现它是可推断的。
fsequential!
是有时称为 "lispy tuple programming" 的示例,在该示例中,您一次迭代处理一个参数,直到所有可变参数参数都用完。这是一个强大的范例,比使用数组的 for
循环允许更灵活的推理(因为它允许 Julia 为每个 "loop iteration" 编译单独的代码)。然而,它通常只有在容器中的元素数量相当少时才有用,否则你最终会得到非常长的编译时间。
类型参数 F
和 Fs
看起来没有必要,但它们旨在强制 Julia 为您传入的特定函数专门化代码。