逐行读取文件的最快方法是什么



我用Python编写了一段代码,逐行读取文件,并执行一些平均和求和操作。

我需要加快速度的建议。

pressurefile中的行数目前为945670(会更高)。

原始代码这是我发布的原始版本。根据你的建议,我正在优化代码,并最终发布了最新版本。

def time_average():
try:
filename = mem.pressurefile
navg = mem.NFRAMES
dz = mem.dz
zlo = mem.zlo
NZ = mem.NZ
mass = mem.mass
dens_fact = amu_to_kg / (mem.slab_V * ang3_to_m3)

array_pxx = np.zeros([NZ,1])
array_pyy = np.zeros([NZ,1])
array_pzz = np.zeros([NZ,1])
array_ndens = np.zeros([NZ,1])

array_density = np.zeros([NZ,1])
array_enthalpy = np.zeros([NZ,1])
array_surf_tens = np.zeros([NZ,1])

counter = 0
with open(filename) as f:
for line in f:
line.strip("n")
#content = [_ for _ in line.split()]
content = line.split()
if len(content) == 7:
z = float(content[3]) - zlo
pxx = float(content[4])
pyy = float(content[5])
pzz = float(content[6])

loc = math.floor(z/dz)
if loc >= NZ:
loc = loc - NZ
elif loc < 0:
loc = loc + NZ   
#print(z, loc, zlo)

array_pxx[loc] += pxx
array_pyy[loc] += pyy
array_pzz[loc] += pzz
array_ndens[loc] += 1
counter += 1
for col in range(NZ):
array_pxx[col] /= navg
array_pyy[col] /= navg
array_pzz[col] /= navg
array_ndens[col] /= navg
array_density[col] = mass * dens_fact * array_ndens[col]

return (array_density, array_enthalpy, array_surf_tens)
except IndexError as err:
writelog (err)
writelog(float(content[3]) , loc, zlo)

到目前为止,我已经尝试了以下选项:

分析:

使用cprofile对主代码进行配置,并确定上面的helper函数对于74.4MB的文件消耗大约10 s。对我来说,这10秒很高。

选项1:胞苷3

使用cython编译如下。

cython3 --embed -o ptythinfile.c ptythinfile.py
gcc -Os -I /usr/include/python3.8 -o ptythinfile ptythinfile.c -lpython3.8 -lpthread -lm -lutil -ldl

这并没有带来任何性能改进。

选项2:C/C++

将整个代码转换为C/C++并进行编译。

事实上,我的第一段代码是用C++编写的,调试是一场噩梦,于是我切换到了python。所以,我不想走这条路。

选项3:Pypy3

我尝试使用pypy3,但遇到了兼容性问题。我有3.8和3.9的蟒蛇,但蟒蛇3一直在寻找3.6,然后我放弃了。

选项4:外部C库

我阅读了关于将helper函数编译为c代码并调用python的教程。这将是我的下一次尝试。

在谷歌上搜索时,我发现了许多选项,如shedskin等。你能指出优化上述代码片段的最佳方法和可能的替代解决方案吗?

更新1:2021年10月21日该代码根据以下专家的意见进行了更新。经过测试,工作良好。然而,平均代码执行时间从约10秒减少到约9.4秒

压力文件的内容是LAMMPS软件的输出,它的前几行看起来像:

ITEM: TIMESTEP
50100
ITEM: NUMBER OF ATOMS
2744
ITEM: BOX BOUNDS pp pp pp
-2.5000000000000000e+01 2.5000000000000000e+01
-2.5000000000000000e+01 2.5000000000000000e+01
-7.5000000000000000e+01 7.5000000000000000e+01
ITEM: ATOMS id x y z c_1[1] c_1[2] c_1[3]
2354 18.8358 -21.02 -70.5731 -21041.8 -3738.18 -2520.84
1708 5.54312 -8.1526 -62.6984 4362.84 -30610.2 -4065.84

最后两行是我们需要处理的。

最新代码

def time_average():
try:
filename = mem.pressurefile
navg = mem.NFRAMES
dz = mem.dz
zlo = mem.zlo
NZ = mem.NZ
mass = mem.mass
dens_fact = amu_to_kg / (mem.slab_V * ang3_to_m3)

array_pxx = np.zeros([NZ,1])
array_pyy = np.zeros([NZ,1])
array_pzz = np.zeros([NZ,1])
array_ndens = np.zeros([NZ,1])

#array_density = np.zeros([NZ,1])
array_enthalpy = np.zeros([NZ,1])
array_surf_tens = np.zeros([NZ,1])

counter = 0
locList = []
pxxList = []
pyyList = []
pzzList = []
with open(filename) as f:
for line in f:
#line.strip("n")
#content = [_ for _ in line.split()]
content = line.split()
if len(content) == 7:
z = float(content[3]) - zlo
pxx = float(content[4])
pyy = float(content[5])
pzz = float(content[6])

#loc = math.floor(z/dz)
loc = int(z // dz)

if loc >= NZ:
loc = loc - NZ
elif loc < 0:
loc = loc + NZ   
#print(z, loc, zlo)

# Not great but much faster than using Numpy functions
locList.append(loc)
pxxList.append(pxx)
pyyList.append(pyy)
pzzList.append(pzz)
counter += 1
# Very fast list-to-Numpy-array conversion
locList = np.array(locList, dtype=np.int32)
pxxList = np.array(pxxList, dtype=np.float64)
pyyList = np.array(pyyList, dtype=np.float64)
pzzList = np.array(pzzList, dtype=np.float64)
# Fast accumulate
np.add.at(array_pxx[:,0], locList, pxxList)
np.add.at(array_pyy[:,0], locList, pyyList)
np.add.at(array_pzz[:,0], locList, pzzList)
np.add.at(array_ndens[:,0], locList, 1)
array_pxx /= navg
array_pyy /= navg
array_pzz /= navg
array_ndens /= navg
array_density = mass * dens_fact * array_ndens
return (array_density, array_enthalpy, array_surf_tens)
except IndexError as err:
writelog (err)
print(loc)
writelog(float(content[3]) , loc, zlo)

测试计算机规格:
Intel®Xeon(R)W-2255 CPU@3.70GHz×20
RAM:16 GB
NVIDIA Corporation GP107GL[Quadro P620]
64位Ubuntu 20.04.3 LTS

当前的平均代码执行时间约为2.6秒(比原始代码快3倍)信用给用户@JeromeRichard

首先,Python显然不是高效进行此类计算的最佳工具。代码是连续的,大部分时间都花在CPython解释器操作或Numpy内部函数中。

选项1:cython3
这没有带来任何性能改进。

这部分是因为没有启用优化。您需要使用标志-O2甚至-O3。尽管如此,Cython可能不会有太大帮助,因为大部分时间都花在了这个特定代码中的CPython到Numpy调用上。

选项2:C/C++将整个代码转换为C/C++并进行编译。事实上,我的第一段代码是用C++编写的,调试是一场噩梦,于是我切换到了python。所以,我不想走这条路。

您不需要移植所有代码。您只能重写像这样的性能关键函数,并将它们放在专用的CPython模块中(即编写C/C++扩展)。然而,这个解决方案需要处理低级别的CPython内部。Cython可能有助于解决这一问题:AFAIK,您可以使用Cython从Cython函数调用C++函数,Cython有助于轻松执行CPython和C++函数之间的接口。简单的函数接口应该有助于使代码更易于阅读和维护。尽管如此,我同意这并不好,但C++代码可以比CPython快至少一个数量级来进行计算。。。

在谷歌搜索中,我发现了许多选项,如shedskin等

ShedSkin不再积极开发。我怀疑这样的项目对您的情况是否有帮助,因为代码非常复杂,并且使用Numpy。

Numba在这种情况下理论上会有很大帮助。然而,字符串还没有得到很好的支持(即解析)。

您能指出优化上述代码片段的最佳方法以及加快速度的可能替代解决方案吗?

array_pxx[loc] += pxx这样的行非常慢,因为解释器需要在内部调用C Numpy函数,该函数执行许多不必要的操作:绑定/类型检查、类型转换、分配/释放、引用计数等。这样的操作非常慢(比C++慢1000倍)。避免这种情况的一个解决方案是,在纯Python循环中使用纯Python列表(至少在代码无法有效向量化时)。您可以有效地将列表转换为Numpy数组,并使用np.add.at执行累加。这里有一个改进的实现:

def time_average():
try:
filename = mem.pressurefile
navg = mem.NFRAMES
dz = mem.dz
zlo = mem.zlo
NZ = mem.NZ
mass = mem.mass
dens_fact = amu_to_kg / (mem.slab_V * ang3_to_m3)

array_pxx = np.zeros([NZ,1])
array_pyy = np.zeros([NZ,1])
array_pzz = np.zeros([NZ,1])
array_ndens = np.zeros([NZ,1])

#array_density = np.zeros([NZ,1])
array_enthalpy = np.zeros([NZ,1])
array_surf_tens = np.zeros([NZ,1])

counter = 0
locList = []
pxxList = []
pyyList = []
pzzList = []
with open(filename) as f:
for line in f:
#line.strip("n")
#content = [_ for _ in line.split()]
content = line.split()
if len(content) == 7:
z = float(content[3]) - zlo
pxx = float(content[4])
pyy = float(content[5])
pzz = float(content[6])

#loc = math.floor(z/dz)
loc = int(z // dz)

if loc >= NZ:
loc = loc - NZ
elif loc < 0:
loc = loc + NZ   
#print(z, loc, zlo)

# Not great but much faster than using Numpy functions
locList.append(loc)
pxxList.append(pxx)
pyyList.append(pyy)
pzzList.append(pzz)
counter += 1
# Very fast list-to-Numpy-array conversion
locList = np.array(locList, dtype=np.int32)
pxxList = np.array(pxxList, dtype=np.float64)
pyyList = np.array(pyyList, dtype=np.float64)
pzzList = np.array(pzzList, dtype=np.float64)
# Fast accumulate
np.add.at(array_pxx[:,0], locList, pxxList)
np.add.at(array_pyy[:,0], locList, pyyList)
np.add.at(array_pzz[:,0], locList, pzzList)
np.add.at(array_ndens[:,0], locList, 1)
array_pxx /= navg
array_pyy /= navg
array_pzz /= navg
array_ndens /= navg
array_density = mass * dens_fact * array_ndens
return (array_density, array_enthalpy, array_surf_tens)
except IndexError as err:
writelog (err)
print(loc)
writelog(float(content[3]) , loc, zlo)

在我的机器上,此代码的总体速度大约快3倍。但是请注意,它应该占用更多的内存(由于列表的原因)。

剩余的大部分时间用于字符串转换(25%)、字符串拆分(20-25%)、列表附加(17%)和CPython解释器本身(如导入模块)(20%)。I/O操作只占用总时间的一小部分(在SSD上或操作系统缓存文件时)。只要使用纯Python代码(使用CPython),优化这一点就很有挑战性。

使用genfromtxt可以轻松完成读取文件的第一步。这会逐行读取文件,对其进行拆分(就像您所做的那样),将结果收集到列表列表中,并在最后生成数组。pandas.read_csv更快,至少在使用c模式时是这样,对于大文件可能值得一试。

制作一个结构化数组,保留第一列的整数性质。通过字段名称(如数据类型中指定的)访问"列":

In [30]: data = np.genfromtxt('stack69665939.py',skip_header=9, dtype=None)
In [31]: data
Out[31]: 
array([(2354, 18.8358 , -21.02  , -70.5731, -21041.8 ,  -3738.18, -2520.84),
(1708,  5.54312,  -8.1526, -62.6984,   4362.84, -30610.2 , -4065.84)],
dtype=[('f0', '<i8'), ('f1', '<f8'), ('f2', '<f8'), ('f3', '<f8'), ('f4', '<f8'), ('f5', '<f8'), ('f6', '<f8')])

或者将所有值加载为float,生成(N,7)2d数组:

In [32]: data = np.genfromtxt('stack69665939.py',skip_header=9)
In [33]: data
Out[33]: 
array([[ 2.35400e+03,  1.88358e+01, -2.10200e+01, -7.05731e+01,
-2.10418e+04, -3.73818e+03, -2.52084e+03],
[ 1.70800e+03,  5.54312e+00, -8.15260e+00, -6.26984e+01,
4.36284e+03, -3.06102e+04, -4.06584e+03]])

usecols指定为仅[3,4,5,6]可能会节省一些时间。你似乎只是对这个数据感兴趣:

In [35]: z = data[:,3]
In [36]: pxyz = data[:,[4,5,6]]
In [37]: z
Out[37]: array([-70.5731, -62.6984])
In [38]: pxyz
Out[38]: 
array([[-21041.8 ,  -3738.18,  -2520.84],
[  4362.84, -30610.2 ,  -4065.84]])

然后,您似乎对z执行了一些操作来派生loc,并使用它来组合"pxyz"数组的"行"。我不会再尝试重现。

无论如何,通常在处理大型csv文件时,我们一步读取,然后再处理生成的数组或数据帧。阅读时进行处理是可能的,但通常不值得付出努力。

相关内容