3

我在我的代码中观察到“.+=”的意外行为(可能只是我,我对 Julia 很陌生)。考虑以下示例:

julia> b = fill(zeros(2,2),1,3)
       1×3 Array{Array{Float64,2},2}:
       [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]

julia> b[1] += ones(2,2)
       2×2 Array{Float64,2}:
       1.0  1.0
       1.0  1.0

julia> b
       1×3 Array{Array{Float64,2},2}:
       [1.0 1.0; 1.0 1.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]

julia> b[2] .+= ones(2,2)
       2×2 Array{Float64,2}:
       1.0  1.0
       1.0  1.0

julia> b
       1×3 Array{Array{Float64,2},2}:
       [1.0 1.0; 1.0 1.0]  [1.0 1.0; 1.0 1.0]  [1.0 1.0; 1.0 1.0]

可以看出,最后一条命令不仅改变了 b[2] 的值,还改变了 b[3] 的值,而 b[1] 保持与之前(*)相同,我们可以确认运行:

julia> b[2] .+= ones(2,2)
       2×2 Array{Float64,2}:
       2.0  2.0
       2.0  2.0

julia> b
       1×3 Array{Array{Float64,2},2}:
       [1.0 1.0; 1.0 1.0]  [2.0 2.0; 2.0 2.0]  [2.0 2.0; 2.0 2.0]

现在,简单地使用“+=”代替我可以获得我对“.+=”的预期行为,即:

julia> b = fill(zeros(2,2),1,3); b[2]+=ones(2,2); b
       1×3 Array{Array{Float64,2},2}:
       [0.0 0.0; 0.0 0.0]  [1.0 1.0; 1.0 1.0]  [0.0 0.0; 0.0 0.0]

谁能解释我为什么会这样?我当然可以只使用 +=,或者可能与数组数组不同的东西,但是因为我正在努力提高速度(我有一个代码需要执行这些操作数百万次,并且需要在更大的矩阵上执行)和 . += 速度要快得多,如果我仍然可以利用此功能,我想了解一下。谢谢大家!

编辑: (*) 显然只是因为 b[1] 不为零。如果我运行:

julia> b = fill(zeros(2,2),1,3); b[2]+=ones(2,2);
julia> b[1] .+= 10 .*ones(2,2); b
       [10.0 10.0; 10.0 10.0]  [1.0 1.0; 1.0 1.0]  [10.0 10.0; 10.0 10.0]

您可以看到只有零值发生了变化。这打败了我。

4

2 回答 2

5

这是由于多种因素的综合作用而发生的。让我们试着让事情变得更清楚。

首先,b = fill(zeros(2,2),1,3)不会zeros(2,2)为 的每个元素创建一个新的b;相反,它会创建一个 2x2 的零数组,并将 的所有元素设置b为该唯一数组。简而言之,这条线的行为等同于

z = zeros(2,2)
b = Array{Array{Float64,2},2}(undef, 1, 3)
for i in eachindex(b)
    b[i] = z
end

因此,修改z[1,1]或其中任何一个b[i,j][1,1]也会修改其他值。为了说明这一点:

julia> b = fill(zeros(2,2),1,3)
1×3 Array{Array{Float64,2},2}:
 [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]

# All three elements are THE SAME underlying array
julia> b[1] === b[2] === b[3]
true

# Mutating one of them mutates the others as well
julia> b[1,1][1,1] = 42
42

julia> b
1×3 Array{Array{Float64,2},2}:
 [42.0 0.0; 0.0 0.0]  [42.0 0.0; 0.0 0.0]  [42.0 0.0; 0.0 0.0]

第二,b[1] += ones(2,2)相当于b[1] = b[1] + ones(2,2)。这意味着一系列操作:

  1. 创建一个新数组(我们称之为tmp)来保存b[1]ones(2,2)
  2. b[1]被反弹到该新数组,从而失去与z(或b.

这是经典主题的变体,尽管两者都=在符号中包含符号,但变异和分配不是一回事。再次说明:

julia> b = fill(zeros(2,2),1,3)
1×3 Array{Array{Float64,2},2}:
 [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]

# All elements are THE SAME underlying array
julia> b[1] === b[2] === b[3]
true

# But that connection is lost when `b[1]` is re-bound (not mutated) to a new array
julia> b[1] = ones(2,2)
2×2 Array{Float64,2}:
 1.0  1.0
 1.0  1.0

# Now b[1] is no more the same underlying array as b[2]
julia> b[1] === b[2]
false

# But b[2] and b[3] still share the same array (they haven't be re-bound to anything else)
julia> b[2] === b[3]
true

第三,b[2] .+= ones(2,2)是完全不同的野兽。它并不意味着将任何东西重新绑定到新创建的数组;相反,它会在适当的位置改变数组b[2]。它实际上表现得像:

for i in eachindex(b[2])
    b[2][i] += 1  # or b[2][i] = b[2][i] + 1
end

b它本身甚至都不会b[2]重新绑定到任何东西,只有它的元素被原地修改。在您的示例中,这b[3]也会影响,因为两者b[2]b[3]都绑定到同一个底层数组。

于 2020-11-24T12:20:46.550 回答
1

因为b填充了相同的矩阵,而不是 3 个相同的矩阵。.+=改变矩阵的内容,从而b改变所有的内容。+=另一方面,创建一个新矩阵并将其分配回 b[1]。要查看这一点,您可以使用===运算符:

b = fill(zeros(2,2),1,3)
b[1] === b[2] # true
b[1] += zeros(2, 2) # a new matrix is created and assigned back to b[1]
b[1] == b[2] # true, they are all zeros
b[1] === b[2] # false, they are not the same matrix

实际上在fill函数的帮助信息中有一个例子正是指出了这个问题。您可以通过?fill在 REPL 中运行来找到它。

  ...

  If x is an object reference, all elements will refer to the same object:

  julia> A = fill(zeros(2), 2);
  
  julia> A[1][1] = 42; # modifies both A[1][1] and A[2][1]
  
  julia> A
  2-element Array{Array{Float64,1},1}:
   [42.0, 0.0]
   [42.0, 0.0]

有多种方法可以创建一组独立矩阵。一种是使用列表理解:

c = [zeros(2,2) for _ in 1:1, _ in 1:3]
c[1] === c[2] # false
于 2020-11-24T12:22:39.400 回答