jhe*_*del 5 parsing text julia
XYZ 格式的文本文件在化学中很常见。这些可以是非常大的纯文本文件,其中包含许多原子的坐标序列。这是一个两帧的示例,但一般来说可能有一百万个:
9
This is always a comment line
O -0.47895617997126 -2.66640293337835 -1.49681666046534
H 0.44977991701355 -2.60732102342602 -1.12187699450036
H -0.47021693081323 -3.17753067276769 -2.30844425585481
O 1.77621192889291 -0.57453626042269 2.55501382514084
H 1.89122710803544 -0.41488971167761 3.50755810540804
H 1.63216983845037 0.32650413720929 2.13442202830578
O 2.09888701915665 -2.00704728106228 -0.54677363666735
H 1.97657049717652 -1.12593867861129 -0.98496874845379
H 2.42388964325074 -1.74615001969454 0.33580032962549
[there might be new lines here but everything before the next frame is ignored]
9
Again, always a comment line
O -0.46951624407696 -2.67861471918262 -1.48616321880993
H 0.4349320578306 -2.59324562286125 -1.15158631652204
H -0.50124399561371 -3.21321032298656 -2.30894245367388
O 1.77946051665009 -0.57508897951565 2.54905851620706
H 1.84047894070088 -0.38790923183093 3.49683088026427
H 1.62214984757904 0.28130906816753 2.15144936348414
O 2.1037484479049 -2.00698121080077 -0.53624845433526
H 2.00667363126475 -1.10227312432539 -1.00287595161132
H 2.40066596189013 -1.75997079252843 0.34324093661981
Run Code Online (Sandbox Code Playgroud)
目标是将其解读为三个不同的Arrays。什么类型的数组并不特别重要,但它们应该包含所有标题,其中包含框架中的原子数和注释行。下一个应该包含每一帧的原子标签,最后所有坐标都应该读入 anArray{Float64, 3}或Array{Arrray{Float64, 2}, 1。我更喜欢后者,但如果使用前者更有效,那就没问题了。
我尝试了两种三种不同的方法,它们的性能几乎相同。我逐行读取文件并将每一行解析为适当的数组,并用于push!分配新的内存。我将整个文件读入一个字符串,然后再次解析该字符串,动态分配内存。最后,我尝试将整个文件读入一个字符串,然后使用第一帧中的原子数来预先分配所有带有零或空字符串的数组。令我惊讶的是,对于包含 400,000 个帧(每个帧有 30 个原子)的 650MB 文件,预分配方法并没有更快,甚至可能更慢。
这是我的版本代码,它首先将文件读入字符串,然后解析该字符串。
function read_xyz(ifile::String)
"""
Reads in an xyz file of possibly multiple geometries, returning the header, atom labels,
and coordinates as arrays of strings and Float64s for the coordinates.
"""
@time file_contents = readlines(ifile)
header = Array{String, 1}()
atom_labels = Array{Array{String, 1}, 1}()
geoms = Array{Array{Float64, 2}, 1}()
for (i, line) in enumerate(file_contents)
if isa(tryparse(Int, line), Int)
# allocate the geometry for this frame
N = parse(Int, line)
head = string(N)
labels = String[]
# store the header for this frame
head = string(line, file_contents[i+1])
i += 1
push!(header, head)
# loop through the geometry storing the vectors and atom labels as you go
geom = zeros((3, N))
for j = 1:N
coords = split(file_contents[i+1])
i += 1
push!(labels, coords[1])
geom[:,j] = parse.(Float64, coords[2:end])
end
push!(geoms, geom)
push!(atom_labels, labels)
end
end
return header, atom_labels, geoms
end
Run Code Online (Sandbox Code Playgroud)
下面是在包含 400,000 个帧(每个帧有 30 个原子)的 650MB 文件上使用此函数的时序。
julia> @time header, labels, geoms = read_xyz("data/1body_forces.dat");
6.804485 seconds (25.22 M allocations: 1.226 GiB, 11.78% gc time)
33.436732 seconds (88.82 M allocations: 7.368 GiB, 23.17% gc time)
julia> @time header, labels, geoms = read_xyz("data/1body_forces.dat");
9.216689 seconds (25.22 M allocations: 1.226 GiB, 28.39% gc time)
36.765633 seconds (88.82 M allocations: 7.368 GiB, 29.41% gc time)
Run Code Online (Sandbox Code Playgroud)
因此,很明显,正在进行大量垃圾收集,这需要大量时间并且速度非常慢。将文件读入字符串的部分似乎分配的内存是应有的内存的两倍。我不确定这是为什么。
我想接下来要尝试的就是将文件读入字符串,然后以并行块的形式解析文件。我希望得到一些输入,至少有助于解决以下问题:我的解析似乎会产生大量必须进行垃圾收集的中间内存,而我实际上不确定如何解决此问题。
非常感谢任何优化此功能的帮助。
我认为分配的一个重要来源是这一行(我没有检查它 - 这只是一个猜测):
geom[:,j] = parse.(Float64, coords[2:end])
Run Code Online (Sandbox Code Playgroud)
你可以将其重写为:
geom[1,j] = parse.(Float64, coords[2])
geom[2,j] = parse.(Float64, coords[3])
geom[3,j] = parse.(Float64, coords[4])
Run Code Online (Sandbox Code Playgroud)
事情应该会变得更快(我已经展开了循环,因为在这种情况下,我认为最好,或者你可以使用循环,但重点是分配parse.(Float64, coords[2:end])两次:1)一次使用coords[2:end]它创建一个副本,一次使用parse.它使用广播创建临时向量)。
还有这一行:
head = string(N)
Run Code Online (Sandbox Code Playgroud)
可能会被删除,因为您以后不再使用它(但也许编译器已经优化了它)。
也作为一般建议,而不是:
for (i, line) in enumerate(file_contents)
Run Code Online (Sandbox Code Playgroud)
while我认为使用循环i作为计数器会更好。问题是,在当前的循环中,您会isa(tryparse(Int, line), Int)为每一行运行,并且大多数情况下这是错误的,同时会增加tryparse您的成本(换句话说,您基本上处理每行两次,而使用while循环则只会处理每行一次)。