【问题标题】:What is fastest way to read files line by line?逐行读取文件的最快方法是什么?
【发布时间】:2021-12-08 10:51:37
【问题描述】:

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

我需要加快速度的建议。

pressurefile 中的行数目前为 945,670(它会更高)。

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

    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 对主要代码进行概要分析,并确定上述辅助函数对于 74.4MB 文件消耗约 10 秒。对我来说,这 10 秒很长。

选项 1:cython3

使用 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 并遇到了兼容性问题。我有python3.8和3.9,但是pypy3一直在找3.6然后我放弃了。

选项 4:外部 C 库

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

在 google 中搜索我发现了很多选项,例如 shedskin 等。您能否指出优化上述代码 sn-p 的最佳方法以及可能的替代解决方案以加快速度?

更新 1:2021 年 10 月 21 日至 2021 年 代码是根据下面专家的 cmets 更新的。测试并运行良好。然而,平均代码执行时间从 ~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
内存:16 GB
NVIDIA Corporation GP107GL [Quadro P620]
64位 Ubuntu 20.04.3 LTS

当前平均代码执行时间约为 2.6 秒(比原来快 3 倍) 感谢用户@JeromeRichard

【问题讨论】:

  • line.strip("\n") 什么都不做。您需要将结果分配回line
  • 很难说没有看到你的文件,但一般来说,如果数据是某种类似于 csv 的东西,像 numpy.genfromtxt 这样的东西会胜过任何普通的 python 文件读取。随着计算手段等的额外好处,使用后续的 numpy 数组也会更快。
  • @Barmar:他们可以删除它;无论如何,下一行调用.split(),之后再也不会使用line;由于 no-arg split 无论如何都会隐式跳过前导/尾随空格,因此即使将 strip 分配回 line 也是毫无意义的。
  • math.floor(z/dz) 可以只是z // dz
  • 如果进程受 I/O 限制——听起来可能是这样——那么加快速度的唯一方法就是减少它。将整个文件读入内存,这可以很快完成,然后逐行处理它可能会减轻这种情况。

标签: python numpy performance


【解决方案1】:

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

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

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

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

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

在谷歌中搜索,我发现了很多选项,例如脱皮等。

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

Numba 在这种情况下理论上可以提供很大帮助。但是,字符串还没有得到很好的支持(即解析)。

您能否指出优化上述代码 sn-p 的最佳方法以及加快速度的可能替代解决方案?

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),优化这一点就具有挑战性。

【讨论】:

    【解决方案2】:

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

    制作一个保留第一列整数性质的结构化数组。通过字段名称(在 dtype 中指定)访问“列”:

    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')])
    

    或将所有值加载为浮点数,制作一个 (N,7) 二维数组:

    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 文件时,我们会一步读取,然后再处理生成的数组或数据帧。阅读时进行处理是可能的,但通常不值得。

    【讨论】:

    • 输入文件不是带有可以轻易丢弃的标头的csv文件。文件中间有几行包含像“ITEM: NUMBER OF ATOMS”这样的字符串或几行整数。该文件似乎是chemical table file。因此,np.genfromtxt 不幸在这里失败了。
    • 如果文件是化学文件格式文件,那么已经有一个库可以读取和解析它(提到here)。通常情况下,使用 input-process-output pattern 和预先存在的库可以让您以相对较少的工作量获得高性能的解决方案。
    猜你喜欢
    • 2011-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-10
    • 1970-01-01
    • 2016-02-14
    • 2011-08-13
    相关资源
    最近更新 更多