dou*_*leE 12 python algorithm performance large-files large-data
我需要生成一个非常大的文本文件.每一行都有一个简单的格式:
Seq_num<SPACE>num_val
12343234 759
Run Code Online (Sandbox Code Playgroud)
我们假设我将生成一个包含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)
Run Code Online (Sandbox Code Playgroud)对于超过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)
Run Code Online (Sandbox Code Playgroud)注意:
所以方法1必须花费更少的时间.什么提示我缺少什么?
Tom*_*art 14
很多与远不如在技术上非常模糊的术语:)基本上,如果你无法衡量它,你不能改进它.
为简单起见,我们有一个简单的基准,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 %f\n" % (seq_id, num_val)
data_file.write(line)
end = datetime.now()
print("elapsed time %s" % (end - start))
Run Code Online (Sandbox Code Playgroud)
loop2.py 2 for 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 %f\n" % (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))
Run Code Online (Sandbox Code Playgroud)
当我在我的计算机上运行这两个脚本(使用SSD驱动器)时,我得到的结果如下:
$ python3 loop1.py
elapsed time 0:00:00.684282
$ python3 loop2.py
elapsed time 0:00:00.766182
Run Code Online (Sandbox Code Playgroud)
每个测量值可能略有不同,但正如直觉所暗示的那样,第二个测量值稍微慢一些.
如果我们想要优化写入时间,我们需要检查手册Python如何实现写入文件.对于文本文件,该open()函数应该使用BufferedWriter.该open函数接受第三个参数,即缓冲区大小.这是有趣的部分:
传递0以切换缓冲关闭(仅允许在二进制模式下),1选择行缓冲(仅在文本模式下可用),并且整数> 1以指示固定大小的块缓冲区的大小(以字节为单位).如果没有给出缓冲参数,则默认缓冲策略的工作方式如下:
二进制文件以固定大小的块缓冲; 使用启发式方法选择缓冲区的大小,尝试确定底层设备的"块大小"并回退到io.DEFAULT_BUFFER_SIZE.在许多系统上,缓冲区通常为4096或8192字节长.
所以,我们可以修改loop1.py和使用行缓冲:
data_file = open('file.txt', 'w', 1)
Run Code Online (Sandbox Code Playgroud)
事实证明这很慢:
$ python3 loop3.py
elapsed time 0:00:02.470757
Run Code Online (Sandbox Code Playgroud)
为了优化写入时间,我们可以根据需要调整缓冲区大小.首先,我们以字节为单位检查行大小:len(line.encode('utf-8')),这给出了11字节.
将缓冲区大小更新为我们预期的行大小(以字节为单位):
data_file = open('file.txt', 'w', 11)
Run Code Online (Sandbox Code Playgroud)
我写得很快:
elapsed time 0:00:00.669622
Run Code Online (Sandbox Code Playgroud)
根据您提供的详细信息,很难估计发生了什么.也许用于估算块大小的启发式方法在您的计算机上运行不正常.无论如何,如果您正在编写固定行长度,则可以轻松优化缓冲区大小.您可以通过利用来进一步优化对文件的写入flush().
结论:通常,为了更快地写入文件,您应该尝试编写与文件系统上的块大小相对应的大量数据 - 这正是Python方法open('file.txt', 'w')正在尝试做的事情.在大多数情况下,您使用默认值是安全的,微基准测试的差异是微不足道的.
您正在分配大量的字符串对象,这些对象需要由GC收集.根据@ kevmo314的建议,为了执行公平的比较,您应该禁用GC loop1.py:
gc.disable()
Run Code Online (Sandbox Code Playgroud)
由于GC可能会尝试在迭代循环时删除字符串对象(您没有保留任何引用).虽然秒方法保持对所有字符串对象的引用,GC最后收集它们.
ama*_*anb 12
以下是@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 %f\n" %(seq_id, num_val)
f.write(line)
print('Execution time: %s seconds' % (time.time() - start_time))
#Execution time: 2.6448447704315186 seconds
Run Code Online (Sandbox Code Playgroud)
注意:在list下面的两个场景中,我初始化了一个空列表,data_lines如:[]而不是使用list().原因是:[]大约快3倍list().以下是对此行为的解释:为什么[]比list()更快?.讨论的主要内容是:虽然[]创建为字节码对象并且是单个指令,但它list()是一个单独的Python对象,它还需要名称解析,全局函数调用以及必须参与堆栈来推送参数.
使用timeit模块中的timeit()函数,这里是比较:
import timeit import timeit
timeit.timeit("[]") timeit.timeit("list()")
#0.030497061136874608 #0.12418613287039193
Run Code Online (Sandbox Code Playgroud)
第二种情况:循环范围(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 %f\n" %(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
Run Code Online (Sandbox Code Playgroud)
第三种情况:循环列表理解并写入文件
借助Python强大而紧凑的列表推导,可以进一步优化流程:
import time
import random
start_time = time.time()
with open('test.txt' ,'w') as f:
data_lines = ["%i %f\n" %(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
Run Code Online (Sandbox Code Playgroud)
在多次迭代中,与前两种情况相比,在这种情况下,我总是收到较低的执行时间值.
#Iteration 2: Execution time: 2.496004581451416 seconds
Run Code Online (Sandbox Code Playgroud)
现在问题出现了:为什么列表推导(和一般列表)比顺序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
Run Code Online (Sandbox Code Playgroud)
这是一个for在函数中反汇编的循环代码对象的示例test:
#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
Run Code Online (Sandbox Code Playgroud)
如果可能的话,上面的比较显示了更多的"活动",在for循环的情况下.例如,注意循环函数调用中对append()方法的附加for函数调用.要了解有关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)
Run Code Online (Sandbox Code Playgroud)
使用的执行时间越长,flush()可归因于数据的处理方式.此函数将数据从程序缓冲区复制到操作系统缓冲区.这意味着如果一个文件(例如test.txt在这种情况下)被多个进程使用并且大块数据被添加到文件中,则不必等待将整个数据写入文件和信息将随时可用.但是为了确保缓冲区数据实际写入磁盘,您还需要添加:os.fsync(f.fileno()).现在,添加os.fsync()将执行时间增加至少10次(我没有经历过整个时间!),因为它涉及将数据从缓冲区复制到硬盘内存.有关详细信息,请转到此处.
进一步优化:可以进一步优化流程.有可用的库支持multithreading,创建Process Pools和执行asynchronous任务.当函数执行CPU密集型任务并同时写入文件时,这尤其有用.例如,结合threading并list comprehensions给出最快的结果:
import time
import random
import threading
start_time = time.time()
def get_seq():
data_lines = ["%i %f\n" %(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
Run Code Online (Sandbox Code Playgroud)
结论:与顺序for循环和list appends 相比,列表推导提供了更好的性能.这背后的主要原因是在列表推导的情况下执行单指令字节码,这比在循环的情况下将项追加到列表的顺序迭代调用更快for.使用asyncio,threading和ProcessPoolExecutor()可以进一步优化.您还可以使用这些组合来实现更快的结果.使用file.flush()取决于您的要求.当您在多个进程使用文件时需要异步访问数据时,可以添加此函数.但是,如果您还要使用程序的缓冲存储器将数据写入OS的磁盘存储器,则此过程可能需要很长时间os.fsync(f.fileno()).
考虑方法2,我想我可以假设你有所有行的数据(或者至少在大块)之前,你需要把它写入文件.
其他答案很棒,阅读它们确实非常具有形式,但两者都专注于优化文件编写或避免第一个for循环替换列表理解(已知更快).
他们错过了你在for循环中迭代来编写文件的事实,这不是必需的.
而不是做的是,通过增加使用的内存(在这种情况下是可以承受的,因为100百万行的文件将是约600 MB),你可以在一个更有效的方式使用的格式只创建一个字符串或加入的功能python str,然后将大字符串写入文件.还依赖列表理解来获取要格式化的数据.
随着LOOP1和@Tombart的答案循环2,我得到 elapsed time 0:00:01.028567和elapsed time 0:00:01.017042分别.
使用此代码时:
start = datetime.now()
data_file = open('file.txt', 'w')
data_lines = ( '%i %f\n'%(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))
Run Code Online (Sandbox Code Playgroud)
我得到的elapsed time 0:00:00.722788速度提高了约25%.
请注意,这data_lines是一个生成器表达式,因此列表实际上并不存储在内存中,并且该join方法会根据需要生成和使用这些行.这意味着唯一显着占用内存的变量是contents.这也略微减少了运行时间.
如果文本很大,可以在内存中完成所有工作,则可以随时分块.也就是说,格式化字符串并每隔百万行左右写入文件.
结论:
filter过滤列表更快看到这里).format或 join函数一次创建和编码字符串内容.for循环.例如,使用extend列表的函数而不是迭代和使用append.事实上,以前的两点都可以看作是这句话的例子.备注. 虽然这个答案本身可以被认为是有用的,但它并没有完全解决这个问题,这就是为什么问题中的两个循环选项似乎在某些环境中运行得更快的原因.为此,也许下面的@Aiken Drum的回答可以为这件事带来一些启示.
| 归档时间: |
|
| 查看次数: |
1085 次 |
| 最近记录: |