我有一个关于Julia中矩阵乘法的计算问题。事实上,我今天就开始学习茱莉亚了。以前我经常使用Python、numpy等。
然而,我有一个6x6矩阵a,我想将其与我已经拥有的初始向量x1相乘,然后将另一个向量a1添加到乘积中。然后,我使用它的输出向量x2作为同一操作的输入,该操作具有相同的矩阵A,但不同的向量A2等等
即:
- 迭代:x1
- 迭代:x2=Ax1+A1
- 迭代x3=Ax2+A2
- 迭代
所涉及的对象非常小,但问题是,我想做很多次这种操作:大约是~1011到~10<sup+13>的次数。此外,我需要非常高的数值精度,远远高于Float64给出的精度。
我能够写出一种直截了当的方法,它做到了我想要的,而且精确到了我想要。在良好/成熟的python实践中,我预定义了我的数组,并在执行过程中用计算值填充它们。然而,我有一种感觉,我被低效的内存管理所束缚,正如分配的数量可能显示的那样。
每隔一段时间(比如每1000次迭代),我想将当前迭代的向量xI保存到另一个数组m,这是函数的输出和我的结果。
我认为将向量x的值替换为第I次迭代Ax+AI计算的结果是有效的。显然,这个就地操作可以由LinearAlgebra
包的函数mul!
来处理,它使用BLAS来执行mul!(Y, A, B) -> Y
。我也研究了StaticArrays
包,但我不确定这是否会有所帮助。
我没有看到任何计算速度的加快或内存分配的减少。我能以某种方式优化我的代码吗?注意:为了简单起见,在本例中,我在每次迭代中使用相同的向量a。
using CSV
using DataFrames
using Arblib
using LinearAlgebra
setprecision(350) # this is required for the high accuracy
function compute() # global function wrapper, I thought this would be better
dff = rand(BigFloat,(6,6)); # I would usually import a specific external matrix
df2 = Matrix(dff)
vecci = Array{BigFloat}([0,0,0,0,1e-21,1e-23]); # this will be my offset vector a
x1 = Array{BigFloat}([0,0,0,0,1e-21,1e-23]); # this will be my input vector x0
function fold(mat, x, a) # here it goes
numevals = Int(1e6); #here I would put 1e11
numsaves = 10^(Int(log10(numevals))-3) #takes three orders of magnitude less than numevals, this is the number of iterations I want to save in my array
m = zeros(BigFloat,6, Int(numevals/numsaves)); # my output array where I store the x_i
lastone = zeros(BigFloat,6);
newone = zeros(BigFloat,6);
m[1:6] .= x; # first iteration
lastone .= x; # used as input for the computation
for k in 1:(numevals-2)
mul!(newone, mat, lastone); #replaces current newone with the result of matrix multiplication
newone .+= a; #adds the offset vector
lastone .= newone; # prepares for next iteration
if mod(k,numsaves)==0 # every 1e3 iterations it stores in output array
m[1+6*Int(k/numsaves):6+6*Int(k/numsaves)] .= newone
end
end
return m
end
@time out2 = fold(df2, x1, vecci)
end
compute()
输出例如
12.948946 seconds (204.00 M allocations: 11.399 GiB, 13.52% gc time)
6×1000 Matrix{BigFloat}:
0.0 7.03814e+465 1.93614e+953 5.3262e+1440 1.4652e+1928 4.03066e+2415 1.10881e+2903 … 4.9002e+484980 1.34801e+485468 3.70828e+485955 1.02012e+486443 2.80629e+486930
0.0 6.45989e+465 1.77707e+953 4.8886e+1440 1.34482e+1928 3.6995e+2415 1.01771e+2903 4.4976e+484980 1.23726e+485468 3.40361e+485955 9.3631e+486442 2.57572e+486930
0.0 8.17893e+465 2.24997e+953 6.1895e+1440 1.70269e+1928 4.68398e+2415 1.28853e+2903 5.69445e+484980 1.5665e+485468 4.30935e+485955 1.18547e+486443 3.26115e+486930
0.0 6.71808e+465 1.8481e+953 5.08399e+1440 1.39857e+1928 3.84737e+2415 1.05838e+2903 4.67736e+484980 1.28671e+485468 3.53965e+485955 9.73733e+486442 2.67867e+486930
1.0e-21 7.55747e+465 2.07901e+953 5.7192e+1440 1.57331e+1928 4.32808e+2415 1.19062e+2903 5.26177e+484980 1.44748e+485468 3.98191e+485955 1.0954e+486443 3.01336e+486930
1.0e-23 8.44558e+465 2.32332e+953 6.39129e+1440 1.7582e+1928 4.83669e+2415 1.33054e+2903 … 5.88011e+484980 1.61758e+485468 4.44984e+485955 1.22412e+486443 3.36747e+486930
编辑:
多亏了这些评论,我成功地用类型转换和MultiFloat包而不是BigFloat更新了代码。这使速度提高了3倍左右,但也损失了很多精度。但是分配的数量和所需的内存减少了很多,这已经是一个很大的改进了!
using CSV
using DataFrames
using MultiFloats
using LinearAlgebra
using Arblib
setprecision(350) # this is required for the high accuracy OF THE IMPORT ONLY
function compute() # global function wrapper, I thought this would be better
dff = rand(Float64x5,(6,6)); # I would usually import a specific external matrix
df1 = Array{Float64x5}(dff)
df2 = Matrix(df1)
vecci = Array{Float64x5}([0,0,0,0,1e-21,1e-23]); # this will be my offset vector a
x1 = Array{Float64x5}([0,0,0,0,1e-21,1e-23]); # this will be my input vector x0
function fold(mat::Matrix{MultiFloat{Float64, 5}}, x::Vector{MultiFloat{Float64, 5}}, a::Vector{MultiFloat{Float64, 5}}) # here it goes
numevals = Int(1e6); #here I would put 1e11
numsaves = 10^(Int(log10(numevals))-3) #takes three orders of magnitude less than numevals, this is the number of iterations I want to save in my array
m = zeros(Float64x5,6, Int(numevals/numsaves)); # my output array where I store the x_i
lastone = zeros(Float64x5,6);
newone = zeros(Float64x5,6);
m[1:6] .= x; # first iteration
lastone .= x; # used as input for the computation
for k in 1:(numevals-2)
newone .= mat*lastone + a;
lastone .= newone; # prepares for next iteration
if mod(k,numsaves)==0 # every 1e3 iterations it stores in output array
#print(newone)
m[1+6*Int(k/numsaves):6+6*Int(k/numsaves)] .= newone
end
end
return m
end
@time out2 = fold(df2, x1, vecci)
end
compute()
输出现在是
2.898294 seconds (2.00 M allocations: 580.139 MiB, 4.80% gc time)
编辑2:
经过几次尝试和与同事的讨论,我能够通过使用静态数组大大减少分配的数量。我还注意到了类型转换和预分配。然而,与第二个代码示例相比,这并没有导致速度的提高。相反,代码慢了25%。然而,由于内存消耗要低得多,我会坚持使用它。
此外,对于我的用例,我可能会尝试进行收敛研究,以在使用高精度浮点时找到精度和速度的最佳折衷方案。对于常规浮点运算,单个矩阵乘法和向量加法大约需要140ns,这可能与本例中的速度一样快。只有当精度高出几倍(比如Float64x5)时,操作大约需要4µs,这太疯狂了。
我强烈推荐使用TimerOutputs
包来查找代码中的瓶颈。
您的代码只返回了一堆NaN,但如果我减少保存之间的间隔,我可以判断它们在返回NaN之前返回了相同的值。
最重要的是,我认为只要使用MultiFloats,性能就不会有太大的提高。以下代码显著减少了分配,但仅略微提高了速度。我尝试了StaticArrays,它让它运行得更慢,我尝试了Octavian.jl/LoopVectorization.jsl,它在MultiFloats中不太好用。在我的笔记本电脑上,叠加两个Float64x5
大约需要32ns,而一个6x6矩阵向量乘积大约需要6^2倍的时间。显然没有并行加速,没有simd效应,也没有可以利用的多线程。基本上,你需要一个更高精度的浮动,这是模拟友好,我不知道任何。
但我对你的代码做了一些改进。评论:
dff
是一个矩阵。我不知道你为什么要创建df1
和df2
,它们看起来是多余的Float64x5(1e-23)
是不准确的,因为在转换之前存在精度损失。Float64x5("1e-23")
避免了这个问题- 我使
fold
更加通用,现在它将接受不同的数组类型和元素。(另外,我不喜欢嵌套函数,所以我把它移走了。) - 使用整数算术而不是将浮点转换为Int,例如
div(a, b)
而不是Int(a/b)
。并写入10^6
而不是Int(1e6)
- 当索引到矩阵
m
时,使用矩阵索引而不是笨拙的线性索引 - 使用
mul!
来避免分配 - 线路
lastone, newone = newone, lastone
以零成本切换两个矢量,无需复制数据
。。。
function compute()
dff = rand(Float64x5, 6, 6)
vecci = Float64x5.(["0", "0", "0", "0", "1e-21", "1e-23"])
x1 = copy(vecci) # this will be my input vector x0
return fold(dff, x1, vecci)
end
function fold(mat::AbstractMatrix{T}, x::AbstractVector{T}, a::AbstractVector{T}) where {T<:Number}
numevals = 10^6
saveinterval = 1000
numsaves = div(numevals, saveinterval)
m = zeros(T, length(a), numsaves)
lastone = copy(x)
newone = zero(x)
m[:, 1] .= x
for k in 1:(numevals-2)
# >95% of the time is spent on the next two lines, though they have zero allocations.
mul!(newone, mat, lastone)
newone .+= a
(d, r) = divrem(k, saveinterval)
if r == 0
m[:, d+1] .= newone
end
lastone, newone = newone, lastone # prepares for next iteration
end
return m
end