【问题标题】:Improving performance of a function in python提高python中函数的性能
【发布时间】:2013-03-05 12:23:44
【问题描述】:

我有一个这种格式的几个 GB 的文本文件

0 274 593869.99 6734999.96 121.83 1,
0 273 593869.51 6734999.92 121.57 1,
0 273 593869.15 6734999.89 121.57 1,
0 273 593868.79 6734999.86 121.65 1,
0 273 593868.44 6734999.84 121.65 1,
0 273 593869.00 6734999.94 124.21 1,
0 273 593868.68 6734999.92 124.32 1,
0 273 593868.39 6734999.90 124.44 1,
0 273 593866.94 6734999.71 121.37 1,
0 273 593868.73 6734999.99 127.28 1,

我有一个简单的函数可以在 Windows 上的 Python 2.7 中进行过滤。该函数读取整个文件,选择具有相同idtile 的行(第一列和第二列)并返回点列表(x、y、z 和标签)和idtile

tiles_id = [j for j in np.ndindex(ny, nx)] #ny = number of row, nx= number of columns
idtile = tiles_id[0]

def file_filter(name,idtile):
        lst = []
        for line in file(name, mode="r"):
            element = line.split() # add value
            if (int(element[0]),int(element[1])) == idtile:
                lst.append(element[2:])
                dy, dx = int(element[0]),int(element[1])
        return(lst, dy, dx)

文件超过 32 GB,瓶颈是文件的读取。我正在寻找一些建议或示例以加快我的功能(例如:并行计算或其他方法)。

我的解决方案是将文本文件拆分为图块(使用 x 和 y 位置)。该解决方案并不优雅,我正在寻找一种有效的方法。

【问题讨论】:

  • 你能分割文件吗?如果可以的话,您可以通过并行方法实现某些目标,否则不太可能有帮助。此外,即使那样,您也必须使用 SSD 才能提供帮助,因为硬盘驱动器在线性读取时速度最快,而不是四处寻找
  • @entropy。我做到了。我将文件拆分为瓷砖,但我的解决方案并不优雅。我正在寻找更优雅的解决方案。
  • file_filter() 在你的程序中被调用了多少次?
  • 如果你想多次读取这个文件,把作业交给数据库怎么样?
  • @Ber 假设 lts_idtile 是一个 idtile 列表,file_filter() 每次调用 len(lts_idtile)。

标签: python performance optimization


【解决方案1】:

您的“idtile”似乎是按特定顺序排列的。也就是说,样本数据表明,一旦您遍历某个“idtile”并点击下一个,则不可能再次出现带有该“idtile”的行。如果是这种情况,您可能会在完成处理所需的“idtile”并点击不同的“idtile”后打破for 循环。在我脑海中浮现:

loopkiller = false
for line in file(name, mode="r"):
    element = line.split()
    if (int(element[0]),int(element[1])) == idtile:
        lst.append(element[2:])
        dy, dx = int(element[0]),int(element[1])
        loopkiller = true
    elif loopkiller:
        break;

这样,一旦你完成了某个“idtile”,你就会停下来;而在您的示例中,您会一直阅读到文件末尾。

如果您的 idtiles 以随机顺序出现,也许您可​​以尝试先编写文件的有序版本。

此外,单独评估 idtile 的数字可能会帮助您更快地遍历文件。假设您的 idtile 是一个两位数和三位整数的二元组,可能类似于:

for line in file(name, mode="r"):
    element = line.split()
    if int(element[0][0]) == idtile[0]:
        if element[1][0] == str(idtile[1])[0]:
            if element[1][1] == str(idtile[1])[1]:
                if element[1][2] == str(idtile[1])[2]:
                    dy, dx = int(element[0]),int(element[1])
                else go_forward(walk)
            else go_forward(run)
         else go_forward(sprint)
     else go_forward(warp)

【讨论】:

    【解决方案2】:

    我建议比较一下完整阅读过程所用的时间和只阅读行而不对它们做任何事情的时间。如果这些时间很接近,您唯一能做的就是改变方法(拆分文件等),因为您可能可以优化的是数据处理时间,而不是文件读取时间。

    我还在您的代码中看到两个值得修复的时刻:

    with open(name) as f:
        for line in f:
            pass #Here goes the loop body
    
    1. 使用with 明确关闭您的文件。您的解决方案应该可以在 CPython 中运行,但这取决于实现,并且可能并不总是那么有效。

    2. 您将字符串转换为int 两次。这是一个相对缓慢的操作。通过重用结果删除第二个。

    附:它看起来像地球表面上一组点的深度或高度值数组,并且表面被分割成瓷砖。 :-)

    【讨论】:

    • 感谢 ellioh。欢迎提出改进建议。为什么用 open(name) as f: 而不是 for line in open(name):?
    • 修正了一个错字。 :-) “for line in f”,当然。 “with”语句总是关闭文件。在您的代码中,当垃圾收集器删除文件对象时,文件将关闭。在 CPython 中,其引用计数几乎相同,但其他风格的 Python 可能会以其他方式执行此操作,并且文件将保持打开状态,直到对象被垃圾回收,可能更晚。
    【解决方案3】:

    我建议您更改代码,以便您读取一次大文件并为每个磁贴 id 写入(临时)文件。比如:

    def create_files(name, idtiles=None):
        files = {}
        for line in open(name):
             elements = line.split()
             idtile = (int(elements[0]), int(elements[1]))
             if idtiles is not None and idtile not in idtiles:
                 continue
             if idtile not in files:
                 files[idtile] = open("tempfile_{}_{}".format(elements[0], elements[1]), "w")
             print >>files[idtile], line
        for f in files.itervalues():
            f.close()
        return files
    

    create_files() 将返回一个 {(tilex, tiley): fileobject} 字典。

    在写入每一行后关闭文件的变体,以解决“打开的文件过多”错误。此变体返回 {(tilex, tiley: filename} 字典。可能会慢一些。

    def create_files(name, idtiles=None):
        files = {}
        for line in open(name):
             elements = line.split()
             idtile = (int(elements[0]), int(elements[1]))
             if idtiles is not None and idtile not in idtiles:
                 continue
             filename = "tempfile_{}_{}".format(elements[0], elements[1])
             files[idtile] = filename
             with open(filename, "a") as f:
                 print >>f, line
        return files
    

    【讨论】:

    • 所以,您将大文件拆分为较小的文件......如果我没有误导的话,OP 已经做了一些事情。
    • @codeape。返回此错误消息 create_files(name) Traceback (last recent call last): File "", line 1, in File "", line 9, in create_files IOError: [Errno 24]打开的文件太多:“tempfile_3_340”
    • 更新了在每行写入后关闭文件的变体。
    • @codeape 名称文件是否排序?你的代码需要被修复
    • 我不确定我是否理解最后一个问题。我还没有测试过代码,所以是的,它可能需要一些修复。我相信没有什么需要整理的。
    【解决方案4】:

    我的解决方案是将大文本文件拆分为每个 idtile 的许多小二进制文件。要更快地读取文本文件,可以使用 pandas:

    import pandas as pd
    import numpy as np
    n = 400000 # read n rows as one block
    for df in pd.read_table(large_text_file, sep=" ", comment=",", header=None, chunksize=n):
        for key, g in df.groupby([0, 1]):
            fn = "%d_%d.tmp" % key
                with open(fn, "ab") as f:
                    data = g.ix[:, 2:5].values
                    data.tofile(f)
    

    然后你可以通过以下方式获取一个二进制文件的内容:

    np.fromfile("0_273.tmp").reshape(-1, 4)
    

    【讨论】:

      【解决方案5】:

      您可以通过进行字符串比较来避免在每一行上都使用split()int()

      def file_filter(name,idtile):
          lst = []
          id_str = "%d %d " % idtile
          with open(name) as f:
              for line in f:
                  if line.startswith(id_str):
                      element = line.split() # add value
                      lst.append(element[2:])
                      dy, dx = int(element[0]),int(element[1])
          return(lst, dy, dx)
      

      【讨论】:

        【解决方案6】:

        您可以将过滤器转换为生成器函数:

        def file_filter(name):
                lst = []
                idtile = None
                for line in file(name, mode="r"):
                    element = line.split() # add value
                    if idtile is None:
                       idtile = (int(element[0]), int(element[1]))
                    if (int(element[0]), int(element[1])) == idtile:
                        lst.append(element[2:])
                        dy, dx = int(element[0]),int(element[1])
                    else:
                        yield lst, dx, dy
                        lst = []
                        idtile = None
        

        这个函数应该为每个idtile返回一个list_of_data, dx, dy的元组,前提是文件按idtile排序

        新的你可以这样使用它:

        for lst, dx, dy in file_filter('you_name_it'):
            process_tile_data(lst, dx, dy)
        

        【讨论】:

        • 谢谢伯。我跳过了file_filter(name)之后的第二步,对不起
        • 谢谢@Ber。我将研究如何对文件进行排序(从 0,0 到 ...)。
        【解决方案7】:

        速度的主要规则:少做。

        • 您可以创建huge.txt 的排序版本并跟踪idtitle 的位置。因此,如果您搜索 (223872, 23239),您已经知道给定信息在文件中的哪个位置,并且可以跳过之前的所有内容 (file.seek)。有人可能会争辩说,此信息在某种程度上等同于 'INDEX'
        • 您可以使用mmap 将文件映射到内存中。
        • 您可以开始编写“工人”来处理不同的文件/位置
        • 您可以将给定的文件传输到 SQL 服务器之类的东西中,并使用标准 SQL 来检索数据。是的,将 32gig 的数据传输到数据库需要时间,但可以将其视为一种预处理。之后,数据库将使用任何方式比您的方法更快地访问数据。

        小想法:

        • 您可以使用切片而不是 line.split() 来避免大量微小的内存分配。

        如何使用数据库的概述:

        假设你周围有类似PostgreSQL 的东西:

        CREATE TABLE tiles
        (
          tile_x integer,
          tile_y integer,
          x double precision,
          y double precision,
          z double precision,
          flag integer
        );
        

        然后您可以用| 替换输入文件中的所有空格,用 替换所有,(以创建一个漂亮而闪亮的.csv)并将其直接输入数据库:

         COPY "tiles" from "\full\path\to\huge.txt" WITH DELIMITER "|";
        

        然后你可以像这样做一些花哨的事情:

        SELECT * FROM "tiles";
        
        tile_x | tile_y |     x     |     y      |   z    | flag
        -------+--------+-----------+------------+--------+-----
             0 |    274 | 593869.99 | 6734999.96 | 121.83 |    1
             0 |    273 | 593869.51 | 6734999.92 | 121.57 |    1
             0 |    273 | 593869.15 | 6734999.89 | 121.57 |    1
             0 |    273 | 593868.79 | 6734999.86 | 121.65 |    1
             0 |    273 | 593868.44 | 6734999.84 | 121.65 |    1
             0 |    273 |    593869 | 6734999.94 | 124.21 |    1
             0 |    273 | 593868.68 | 6734999.92 | 124.32 |    1
             0 |    273 | 593868.39 |  6734999.9 | 124.44 |    1
             0 |    273 | 593866.94 | 6734999.71 | 121.37 |    1
             0 |    273 | 593868.73 | 6734999.99 | 127.28 |    1 
        

        或者是这样的:

        SELECT * FROM "tiles" WHERE tile_y > 273;
        
        tile_x | tile_y |     x     |     y      |   z    | flag
        -------+--------+-----------+------------+--------+-----
             0 |    274 | 593869.99 | 6734999.96 | 121.83 |    1
        

        【讨论】:

          【解决方案8】:

          这是一些统计数据。随着更多解决方案的出现,我将对其进行更新。以下程序适用于包含问题中重复行的文件。

          import sys
          
          def f0(name, idtile):
              lst = []
              dx, dy = None, None
              with open(name) as f:
                  for line in f:
                      pass
          
          
          """def f0(name, idtile):
              lst = []
              dx, dy = None, None
              with open(name) as f:
                  for line in f:
                      line.split()"""
          
          
          def f1(name, idtile):
              lst = []
              dx, dy = None, None
              with open(name) as f:
                  for line in f:
                      element = line.split() # add value
                      if (int(element[0]),int(element[1])) == idtile:
                          lst.append(element[2:])
                          dy, dx = int(element[0]),int(element[1])
              return(lst, dy, dx)
          
          
          def f2(name, idtile):
              lst = []
              dx, dy = None, None
              with open(name) as f:
                  for line in f:
                      element = line.split() # add value
                      pdx, pdy = int(element[0]),int(element[1])
                      if (pdx, pdy) == idtile:
                          lst.append(element[2:])
                          dy, dx = pdx, pdy
              return(lst, dy, dx)
          
          def f2(name, idtile):
              lst = []
              dx, dy = None, None
              with open(name) as f:
                  for line in f:
                      element = line.split() # add value
                      pdx, pdy = int(element[0]),int(element[1])
                      if (pdx, pdy) == idtile:
                          lst.append(element[2:])
                          dy, dx = pdx, pdy
              return(lst, dy, dx)
          
          
          def f3(name,idtile):
              lst = []
              id_str = "%d %d " % idtile
              with open(name) as f:
                  for line in f:
                      if line.startswith(id_str):
                          element = line.split() # add value
                          lst.append(element[2:])
                          dy, dx = int(element[0]),int(element[1])
              return(lst, dy, dx)
          
          functions = [f0, f1, f2, f3]
          
          functions[int(sys.argv[1])]("./srcdata.txt",(0, 273))
          

          计时的shell脚本很简单:

          #!/bin/sh
          time python ./timing.py 0
          time python ./timing.py 1
          time python ./timing.py 2
          

          我更喜欢以这种方式运行它,以避免以前运行的函数影响其他函数的时间。

          结果是:

          0.02user 0.01system 0:00.03elapsed
          0.42user 0.01system 0:00.44elapsed
          0.32user 0.02system 0:00.34elapsed
          0.33user 0.01system 0:00.35elapsed
          

          好消息:读取文件不是瓶颈删除额外的到 int 的传输是有效的

          坏消息:我现在仍然不知道如何显着优化它。

          【讨论】:

          • 读文件不是瓶颈,问题是你读文件超过100万次
          • 可能我需要先对文件进行排序并按idtiles切片idtiles
          • 是的,那么应该切片。事实上,我认为这是唯一的方法。
          • 我在操作系统上打开另一个问题。如果你想参与,我会放链接
          • 是的,至少我想看看。
          【解决方案9】:

          也许最好和最快的方法是在(大规模)并行系统上使用 map reduce 算法来解决您的问题。

          【讨论】:

            【解决方案10】:

            好的。如果您需要多次执行此操作,您显然需要创建某种索引。但如果这不是一个频繁的活动,最好的办法就是像这样多线程。

            NUMWORKERS = 8
            workerlist = []
            workQ = Queue.Queue()
            
            def file_filter(name,idtile, numworkers):
                for i in range(NUMWORKERS):
                    worker = threading.Thread(target=workerThread, args=(lst,))
                lst = []
                for line in file(name, mode="r"):
                    workQ.put(line)                
                for i in range(NUMWORKERS):
                    workQ.put(None)
                workQ.join()
                return lst , idtile[0], idtile[1]
            
            def workerThread(lst):
                line = workQ.get()
                if not line:
                    return
                element = line.split() # add value
                if (int(element[0]),int(element[1])) == idtile:
                    lst.append(element[2:])
            

            如果这是每个idtile都发生的非常频繁的活动,那么解决方案将大不相同。对多个 idtile 一起执行此操作将为您提供最佳的摊销性能。因为可以在文件的单个循环中处理任意数量的已知 idtile。

            【讨论】:

            • 这是有利的,因为您的 IO 线程不会浪费时间进行计算。这意味着您的 IO 以最大可能的速度运行。
            • 我认为这不会有帮助。更重要的是,我敢打赌这比以普通方式执行要慢得多,至少在 CPython 中是这样。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-10-12
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-06-11
            • 2022-07-13
            相关资源
            最近更新 更多