时间表现在Python中生成非常大的文本文件

我需要生成一个非常大的文本文件。 每一行都有一个简单的格式:

Seq_num<SPACE>num_val
12343234 759

我们假设我将生成1亿行文件。 我尝试了两种方法,令人惊讶的是他们给出了非常不同的时间表现。

  • 超过100米的循环。 在每个循环中,我使用seq_num<SPACE>num_val短字符串,然后将其写入文件。 这种方法需要长时间。

    ## APPROACH 1  
    for seq_id in seq_ids:
        num_val=rand()
        line=seq_id+' '+num_val
        data_file.write(line)
    
  • 超过100米的循环。 在每个循环中,我使用seq_num<SPACE>num_val短字符串,然后将其附加到列表中。 当循环结束时,我遍历列表项并将每个项目写入文件。 这种方法花费的时间少得多

    ## APPROACH 2  
    data_lines=list()
    for seq_id in seq_ids:
        num_val=rand()
        l=seq_id+' '+num_val
        data_lines.append(l)
    for line in data_lines:
        data_file.write(line)
    
  • 注意:

  • 方法2有2个循环而不是1个循环。
  • 我写入方法1和方法2的循环文件。因此,这两步必须相同。
  • 所以方法1必须花费更少的时间。 任何提示我缺少什么?


    考虑到方法2,我想我可以假设你有所有行的数据(或者至少是大块),然后你需要将它写入文件。

    其他的答案很棒,它的确是形成性的,可以读取它们,但都着重于优化文件写入或避免首先用循环替换列表理解(这被称为更快)。

    他们错过了你在for循环中迭代写入文件的事实,这并不是真的必要。

    而不是这样做,通过增加内存的使用(在这种情况下价格合理,因为1亿行文件约为600 MB),您可以通过使用格式化或连接功能来更有效地创建一个字符串python str,然后将大字符串写入文件。 也依靠列表理解来获取数据的格式。

    用@Tombart的答案的循环1和循环2,我分别获得了elapsed time 0:00:01.028567elapsed time 0:00:01.017042

    虽然使用此代码:

    start = datetime.now()
    
    data_file = open('file.txt', 'w')
    data_lines = ( '%i %fn'%(seq_id, random.random()) 
                                for seq_id in xrange(0, 1000000) )
    contents = ''.join(data_lines)
    data_file.write(contents) 
    
    end = datetime.now()
    print("elapsed time %s" % (end - start))
    

    elapsed time 0:00:00.722788 ,大约快了25%。

    请注意, data_lines是一个生成器表达式,因此列表并不真正存储在内存中,并且这些行由join方法根据需要生成和使用。 这意味着显着占据记忆的唯一变量就是contents 。 这也会减少运行时间。

    如果文本很大以完成内存中的所有工作,则可以始终以块分隔。 也就是说,格式化字符串并每隔一百万行写入文件。

    结论:

  • 总是尝试做列表理解而不是简单的循环(列表理解甚至比筛选列表的filter更快)。
  • 如果可能的话,通过内存或实现约束,尝试使用formatjoin函数立即创建和编码字符串内容。
  • 如果可能的话,代码保持可读,使用内置的功能,避免for循环。 例如,使用列表的extend功能而不是迭代和使用append 。 事实上,前面的两点都可以被看作是这句话的例子。
  • 备注。 尽管这个答案本身可以被认为是有用的,但它并没有完全解决这个问题,这就是为什么问题中的两个循环选项似乎在某些环境中运行得更快。 为此,下面的@Aiken Drum的答案可能会为此提供一些启示。


    技术上非常模糊的术语很多而且少得多 :)基本上,如果你不能测量它,你不能改进它。

    为了简单起见,我们有一个简单的基准测试, loop1.py

    import random
    from datetime import datetime
    
    start = datetime.now()
    data_file = open('file.txt', 'w')
    for seq_id in range(0, 1000000):
            num_val=random.random()
            line="%i %fn" % (seq_id, num_val)
            data_file.write(line)
    
    end = datetime.now()
    print("elapsed time %s" % (end - start))
    

    loop2.py与2 for循环:

    import random
    from datetime import datetime
    
    start = datetime.now()
    data_file = open('file.txt', 'w')
    data_lines=list()
    for seq_id in range(0, 1000000):
        num_val=random.random()
        line="%i %fn" % (seq_id, num_val)
        data_lines.append(line)
    for line in data_lines:
        data_file.write(line)
    
    end = datetime.now()
    print("elapsed time %s" % (end - start))
    

    当我在计算机上运行这两个脚本(使用SSD驱动器)时,我收到了如下所示的内容:

    $ python3 loop1.py 
    elapsed time 0:00:00.684282
    $ python3 loop2.py 
    elapsed time 0:00:00.766182
    

    每个度量可能略有不同,但直觉表明,第二个稍微慢一些。

    如果我们想优化写入时间,我们需要检查手册中Python如何实现写入文件。 对于文本文件的open()函数使用BufferedWriter .The open函数接受第三个参数是缓冲区的大小。 以下是有趣的部分:

    通过0切换缓冲关闭(仅在二进制模式下允许),1选择行缓冲(仅在文本模式下可用)以及> 1的整数,以指示固定大小的块缓冲区的大小(以字节为单位)。 如果未给出缓冲参数,则默认缓冲策略的工作方式如下所示:

    二进制文件以固定大小的块进行缓冲; 缓冲区的大小通过试图确定底层设备的“块大小”并回落到io.DEFAULT_BUFFER_SIZE的启发式来选择。 在很多系统上,缓冲区的长度通常为4096或8192字节。

    所以,我们可以修改loop1.py并使用行缓冲:

    data_file = open('file.txt', 'w', 1)
    

    事实证明这很慢:

    $ python3 loop3.py 
    elapsed time 0:00:02.470757
    

    为了优化写入时间,我们可以根据我们的需要调整缓冲区大小。 首先,我们检查以字节为单位的行大小: len(line.encode('utf-8')) ,这给了我11个字节。

    将缓冲区大小更新为我们期望的以字节为单位的行大小后:

    data_file = open('file.txt', 'w', 11)
    

    我得到相当快的写道:

    elapsed time 0:00:00.669622
    

    根据你提供的细节,很难估计发生了什么。 也许用于估算块大小的启发式算法在您的计算机上无法正常工作。 无论如何,如果您正在编写固定行长度,则优化缓冲区大小非常容易。 您可以通过利用flush()来进一步优化对文件的写入。

    结论 :通常为了更快地写入文件,您应该尝试写入与文件系统上的块大小相对应的大量数据 - 这正是Python方法open('file.txt', 'w')是什么试图去做。 在大多数情况下,使用默认值可以保证安全性,但微观基准的差异是微不足道的。

    您正在分配大量的字符串对象,需要由GC收集。 正如@ kevmo314所建议的那样,为了进行公平比较,您应该禁用GC for loop1.py

    gc.disable()
    

    由于GC可能会尝试在遍历循环时删除字符串对象(您没有保留任何引用)。 虽然秒方法保持对所有字符串对象的引用,并且GC在最后收集它们。


    下面是@Tombart的优雅回答以及一些进一步的观察结果的扩展。

    考虑到一个目标:优化从循环读取数据并将其写入文件的过程,让我们开始:

    我将使用with语句在所有情况下打开/关闭文件test.txt 。 该语句在其中的代码块被执行时自动关闭该文件。

    另一个要考虑的重点是Python基于操作系统处理文本文件的方式。 从文档:

    注意 :Python不依赖于底层操作系统的文本文件概念; 所有的处理都由Python自己完成,因此是平台无关的。

    这意味着这些结果在Linux / Mac或Windows操作系统上执行时可能会略有不同。 其他进程可能会同时使用相同的文件,或脚本执行期间文件上发生多个IO进程,通用CPU处理速度等等。

    我为每个案例提供了3个执行时间的案例,并最终找到了进一步优化最有效和最快速案例的方法:

    第一种情况:循环范围(1,1000000)并写入文件

    import time
    import random
    
    start_time = time.time()
    with open('test.txt' ,'w') as f:
        for seq_id in range(1,1000000):
            num_val = random.random()    
            line = "%i %fn" %(seq_id, num_val)
            f.write(line)
    
    print('Execution time: %s seconds' % (time.time() - start_time)) 
    
    #Execution time: 2.6448447704315186 seconds
    

    注意 :在下面的两个list方案中,我初始化了一个空列表data_lines如: []而不是使用list() 。 原因是: []list()快大约3倍。 以下是对此行为的解释:为什么[]比list()更快? 讨论的主要关键是:虽然[]是作为字节码对象创建的,并且是单个指令,但list()是一个单独的Python对象,它也需要名称解析,全局函数调用以及必须涉及的堆栈来推送参数。

    在timeit模块中使用timeit()函数,这里是比较:

    import timeit                 import timeit                     
    timeit.timeit("[]")           timeit.timeit("list()")
    #0.030497061136874608         #0.12418613287039193
    

    第二种情况:循环超过范围(1,1000000),将值附加到空列表中,然后写入文件

    import time
    import random
    
    start_time = time.time()
    data_lines = []
    with open('test.txt' ,'w') as f:
        for seq_id in range(1,1000000):
            num_val = random.random()    
            line = "%i %fn" %(seq_id, num_val)
            data_lines.append(line)
        for line in data_lines:
            f.write(line)
    
    print('Execution time: %s seconds' % (time.time() - start_time)) 
    
    #Execution time: 2.6988046169281006 seconds
    

    第三种情况:遍历列表理解并写入文件

    使用Python强大而紧凑的列表解析功能,可以进一步优化过程:

    import time
    import random
    
    start_time = time.time()
    
    with open('test.txt' ,'w') as f: 
            data_lines = ["%i %fn" %(seq_id, random.random()) for seq_id in range(1,1000000)]
            for line in data_lines:
                f.write(line)
    
    print('Execution time: %s seconds' % (time.time() - start_time))
    
    #Execution time: 2.464804172515869 seconds
    

    在多次迭代中,与前两种情况相比,在这种情况下,我总是收到较低的执行时间值。

    #Iteration 2: Execution time: 2.496004581451416 seconds
    

    现在出现了这样一个问题:为什么列表解析(和一般列表)比顺序for循环更快?

    分析顺序for循环执行和list执行时会发生什么的一个有趣的方法是dis由每个循环生成的code对象并检查内容。 这是一个反汇编列表理解代码对象的例子:

    #disassemble a list code object
    import dis
    l = "[x for x in range(10)]"
    code_obj = compile(l, '<list>', 'exec')
    print(code_obj)  #<code object <module> at 0x000000058DA45030, file "<list>", line 1>
    dis.dis(code_obj)
    
     #Output:
        <code object <module> at 0x000000058D5D4C90, file "<list>", line 1>
      1           0 LOAD_CONST               0 (<code object <listcomp> at 0x000000058D5D4ED0, file "<list>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (range)
              8 LOAD_CONST               2 (10)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               3 (None)
             20 RETURN_VALUE
    

    下面是一个在函数test反汇编的for循环代码对象的例子:

    #disassemble a function code object containing a `for` loop
    import dis
    test_list = []
    def test():
        for x in range(1,10):
            test_list.append(x)
    
    
    code_obj = test.__code__ #get the code object <code object test at 0x000000058DA45420, file "<ipython-input-19-55b41d63256f>", line 4>
    dis.dis(code_obj)
    #Output:
           0 SETUP_LOOP              28 (to 30)
                  2 LOAD_GLOBAL              0 (range)
                  4 LOAD_CONST               1 (1)
                  6 LOAD_CONST               2 (10)
                  8 CALL_FUNCTION            2
                 10 GET_ITER
            >>   12 FOR_ITER                14 (to 28)
                 14 STORE_FAST               0 (x)
    
      6          16 LOAD_GLOBAL              1 (test_list)
                 18 LOAD_ATTR                2 (append)
                 20 LOAD_FAST                0 (x)
                 22 CALL_FUNCTION            1
                 24 POP_TOP
                 26 JUMP_ABSOLUTE           12
            >>   28 POP_BLOCK
            >>   30 LOAD_CONST               0 (None)
                 32 RETURN_VALUE
    

    上面的比较显示了更多的“活动”,如果可能的话,在for循环的情况下。 例如,注意for循环函数调用中对append()方法的append()函数调用。 要详细了解dis调用输出中的参数,请参阅官方文档。

    最后,如前所述,我还使用file.flush()进行了测试,执行时间超过了11 seconds 。 我在file.write()语句前添加f.flush():

    import os
    .
    .
    .
    for line in data_lines:
            f.flush()                #flushes internal buffer and copies data to OS buffer
            os.fsync(f.fileno())     #the os buffer refers to the file-descriptor(fd=f.fileno()) to write values to disk
            f.write(line)
    

    使用flush()的更长的执行时间可以归因于数据处理的方式。 该功能将数据从程序缓冲区复制到操作系统缓冲区。 这意味着如果一个文件(在这种情况下称为test.txt )被多个进程使用,并且大量的数据块被添加到文件中,则不必等待整个数据写入文件这些信息将随时可用。 但要确保缓冲区数据实际写入磁盘,还需要添加: os.fsync(f.fileno()) 。 现在,添加os.fsync()将执行时间增加至少10倍 (我没有坐下来!),因为它涉及将数据从缓冲区复制到硬盘内存。 欲了解更多详情,请点击这里。

    进一步优化 :可以进一步优化流程。 有可用的库支持multithreading ,创建Process Pools和执行asynchronous任务。 当功能执行CPU密集型任务并同时写入文件时,此功能特别有用。 例如, threadinglist comprehensions threading的组合提供了最快的可能结果:

    import time
    import random
    import threading
    
    start_time = time.time()
    
    def get_seq():
        data_lines = ["%i %fn" %(seq_id, random.random()) for seq_id in range(1,1000000)]
        with open('test.txt' ,'w') as f: 
            for line in data_lines:
                f.write(line)
    
    set_thread = threading.Thread(target=get_seq)
    set_thread.start()
    
    print('Execution time: %s seconds' % (time.time() - start_time))
    
    #Execution time: 0.015599966049194336 seconds
    

    结论 :与顺序for循环和list append相比,列表解析提供了更好的性能。 这背后的主要原因是在列表解析的情况下单指令字节码的执行速度比顺序迭代调用将项追加到列表的速度要快,如for循环的情况。 使用asyncio,线程和ProcessPoolExecutor()可以进一步优化。 您也可以使用这些组合来获得更快的结果。 使用file.flush()取决于您的要求。 当多个进程正在使用文件时,您需要对数据进行异步访问时,可以添加此功能。 尽管如果您还使用os.fsync(f.fileno())将程序的缓冲区内存中的数据写入操作系统的磁盘内存,此过程可能需要很长时间。

    链接地址: http://www.djcxy.com/p/31695.html

    上一篇: Time performance in Generating very large text file in Python

    下一篇: Why is it possible to replace sometimes set() with {}?