在嵌套函数中更新静态数组,而不制作临时数组?<Julia>

4

我一直在试图在Julia中使用静态数组,但却一直碰壁。

https://github.com/JuliaArrays/StaticArrays.jl

他们很快,但更新它们很麻烦。这并不奇怪,因为它们本来就是不可变的!
但是一直有人建议我即使需要更新它们也使用静态数组。在我的情况下,静态数组很小,只有长度为3,我有一个它们的向量,但我每次只更新一个长度为3的SVector。

选项1

有一个非常棒的包叫做Setfield,它允许您在Julia中进行SVectors的就地更新。

https://github.com/jw3126/Setfield.jl

抓住问题......它更新本地副本。因此,如果您在嵌套函数中,则会更新本地副本。因此,需要进行一些簿记,因为您必须就地更新本地副本,然后return该副本并更新实际感兴趣的数组。您无法传入所需的数组并直接更新它,至少我找不到方法!现在,我不介意簿记,但我觉得更新本地副本,然后返回值,更新另一个本地副本,然后返回值,最后更新实际数组必须带来速度惩罚。我可能错了。

选项2

让我感到困扰的是,为了更新静态数组,我必须执行以下操作:

exampleSVector::SVector{3,Float64} <-- 只是为了明确其类型和大小

exampleSVector = [value1, value2, value3]

这将更新所需的数组,即使它在函数内部,这很好并且是目标,但如果您在函数内部执行此操作,则会创建一个临时数组。这让我很烦恼,因为我的函数在循环中被调用了400万次以上,因此这会产生大量分配并减慢速度。


我如何在不创建临时数组的情况下更新选项2场景下的SVector?对于选项1场景,我能否更新实际感兴趣的数组而不是本地副本?如果需要一个简单的示例代码,请在评论中说明,我会制作一个。我的想法是可以回答这个问题,但如果需要,我会制作一个示例代码。
编辑:MCVE代码-选项1可行,选项2不可行。
using Setfield
using StaticArrays

struct Keep
    dreaming::Vector{SVector{3,Float64}}
end

function INNER!(vec::SVector{3,Float64},pre::SVector{3,Float64})
    # pretend series of calculations
    for i = 1:3 # illustrate use of Setfield (used in real code for this)
        pre = @set pre[i] = rand() * i * 1000
    end

    # more pretend calculations
    x = 25.0 # assume more calculations equals x
################## OPTION 1 ########################
    vec = @set vec = x * [ pre[1], pre[2], pre[3] ] # UNCOMMENT FOR FOR OPTION 1
    return vec                                      # UNCOMMENT FOR FOR OPTION 1

################## OPTION 2 ########################    
    #vec = x * [ pre[1], pre[2], pre[3] ]           # UNCOMMENT FOR FOR OPTION 2
    #nothing                                        # UNCOMMENT FOR FOR OPTION 2

end

function OUTER!(always::Keep)
    preAllocate = SVector{3}(0.0,0.0,0.0)

    for i=1:length(always.dreaming)

        always.dreaming[i] = INNER!(always.dreaming[i], preAllocate) # UNCOMMENT FOR FOR OPTION 1
        #INNER!(always.dreaming[i], preAllocate)                     # UNCOMMENT FOR FOR OPTION 2
    end
end
code = Keep([zero(SVector{3}) for i=1:5])

OUTER!(code)
println(code.dreaming)

我不明白为什么你需要进行突变。你为什么不每次都创建一个新的 SVector 呢?对于第二个选项,你可以写成 exampleSVector = SVector(value1, value2, value3),这样就不需要分配内存了。在你的 MCVE 代码中,只需写成 vec = vec .* x 即可。顺便说一句:vec 是 Base 库中的函数名称,你不应该将其覆盖。 - DNF
我在你的示例代码中没有看到任何需要改变的地方,但如果你非常必须要改变,MVector 的速度并不比 SVector 慢太多。在某些情况下它们几乎是相似的。(实际上,你也可以用 vec *= x 来代替 vec = vec .* x。) - DNF
2个回答

3
我希望我正确理解了你的问题。由于MWE做了很多大多数都是冗余且有些令人困惑的事情,这让我有点难以理解。
这里似乎有两种不同的解释:要么你真的需要更新(“mutate”)一个SVector,但是你的MWE没有展示为什么需要这样做;要么你已经让自己相信你需要改变它,但实际上你并不需要。
我决定关注第二种情况:你其实不需要“mutate”。从这个角度重写你的代码会大大简化它。
在这里我找不到更新任何静态向量的理由,因此我将其删除了。函数INNER! 的输入行为非常令人困惑。你提供了两个输入,但却没有使用其中任何一个,所以我将这些输入删除了。
function inner()
    pre = @SVector [rand() * 1000i for i in 1:3]
    x = 25
    return pre .* x
end

function outer!(always::Keep)
    always.dreaming .= inner.()  # notice the dot in inner.()
end

code = Keep([zero(SVector{3}) for i in 1:5])
outer!(code)
display(code.dreaming)

这个运行速度快且不会发生内存分配。通常情况下,使用StaticArrays时,请勿尝试更改内容,只需创建新的实例即可。
虽然从您提供的MWE中无法确定是否有合理的原因需要“更改”一个SVector。但如果是这种情况,您可以使用StaticArrays的setindex方法,而不需要Setfield.jl:
julia> v = rand(SVector{3})
3-element SArray{Tuple{3},Float64,1,3}:
 0.4730258499237898 
 0.23658547518737905
 0.9140206579322541 

julia> v = setindex(v, -3.1, 2)
3-element SArray{Tuple{3},Float64,1,3}:
  0.4730258499237898
 -3.1               
  0.9140206579322541

为了澄清:没有感叹号的setindex不会改变其输入,而是创建一个新实例并更改一个索引值。
如果您确实需要"改变",也许您可以制作一个新的MWE来展示这一点。我建议您尝试简化它,因为现在它非常令人困惑。例如,包含类型Keep似乎完全不必要和分散注意力。只需制作SVectorVector并展示您想要做的事情即可。
编辑:以下是基于下面的评论尝试。据我所理解,问题是关于修改SVector的向量。您无法真正改变SVector,但可以使用方便的语法setindex替换它们,其中您可以保留某些元素并更改其他元素:
oldvec = [zero(SVector{3}) for _ in 1:5]
replacevec = [rand(SVector{3}) for _ in 1:5]

现在,我们将oldvec中的每个元素的第二个元素替换为replacevec中对应的元素。首先是一条简明扼要的代码:
oldvec .= setindex.(oldvec, getindex.(replacevec, 2), 2)

然后是一个带有循环的更快的函数:

for i in eachindex(oldvec, replacevec)
    @inbounds oldvec[i] = setindex(oldvec[i], replacevec[i][2], 2)
end

MWE 的问题在于它们并不意味着有意义,只是模拟问题。我发布的 MWE 就是这样做的。更改它会改变问题。然而,也许使用两个 SVectors 是混淆的原因。称为“pre”的那个不是直接与问题有关的部分。我关心的 SVector 是“vec”,我包括“pre”是因为“vec”依赖于“pre”,所以我希望这种依赖关系存在,以防影响任何解决方案。也许我应该将“pre”作为示例的普通“Vector”? - Charlie Crown
我的设计决策是使用结构体来保存所有数据。将事物保存在数组中与将数组保存在结构体中是不同的问题。让更改一直传递到结构体是我面临的问题之一。更改本地数组很容易,但更改不会传递到结构体。 - Charlie Crown
首先,您无法更新或改变vec,只能替换它。我使用了“mutate”这个词,因为它只是_模拟_突变,实际上是替换整个东西。 INNER!(always.dreaming [i],preAllocate)永远不起作用。 always.dreaming的元素不能被突变,您只能突变always.dreaming本身。 vec = x * [pre [1],pre [2],pre [3]]会分配内存,因为[pre [1],pre [2],pre [3]]是一个普通的Vector,每次都会分配内存。 - DNF
使用结构体是可以的,但你在 MWE 中并没有使用该结构体。你的 MWE 只适用于 always.dreaming 这个数组。这个数组被包含在一个结构体中是无关紧要的,反而会分散注意力。 - DNF
这确实是一种不同的思考方式。在不可变结构体中直接拥有静态数组会使它们难以更新,没错。但是,在不可变结构体内部的普通向量中使用静态数组并不是问题。您可以通过替换外部向量的元素来改变 svectors 的向量。将 svector 视为标量。您无法更新标量,只能替换它。这是同样的道理。 - DNF
显示剩余5条评论

2

有两种类型的静态数组 - 可变的(类型名称以 M 开头)和不可变的(以 S 开头) - 只需使用可变的那种!请看下面的示例:

julia> mut = MVector{3,Int64}(1:3);

julia> mut[1]=55
55

julia> mut
3-element MArray{Tuple{3},Int64,1,3}:
 55
  2
  3

julia> immut = SVector{3,Int64}(1:3);

julia> inmut[1]=55
ERROR: setindex!(::SArray{Tuple{3},Int64,1,3}, value, ::Int) is not defined.

让我们来看一些简单的基准测试(普通数组,可变静态和不可变静态):

using BenchmarkTools

julia> ord = [1,2,3];

julia> @btime $ord.*$ord;
  39.680 ns (1 allocation: 112 bytes)
3-element Array{Int64,1}:
 1
 4
 9



julia> @btime $mut.*$mut
  8.533 ns (1 allocation: 32 bytes)
3-element MArray{Tuple{3},Int64,1,3}:
 3025
    4
    9


julia> @btime $immut.*$immut
  2.133 ns (0 allocations: 0 bytes)
3-element SArray{Tuple{3},Int64,1,3}:
 1
 4
 9

我一直被告知要使用静态的,因为它们据说更快? - Charlie Crown
1
是的 - 它们更快,但我理解您需要可变的对象。我在我的答案中更新了一个基准测试。通常,创建一个新的不可变对象并丢弃旧对象比操作可变对象的状态要快。 - Przemyslaw Szufel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接