asf*_*107 9 python perl performance
我有一个包含一百万行的gzip压缩文件:
$ zcat million_lines.txt.gz | head
1
2
3
4
5
6
7
8
9
10
...
Run Code Online (Sandbox Code Playgroud)
我处理此文件的Perl脚本如下:
# read_million.pl
use strict;
my $file = "million_lines.txt.gz" ;
open MILLION, "gzip -cdfq $file |";
while ( <MILLION> ) {
chomp $_;
if ($_ eq "1000000" ) {
print "This is the millionth line: Perl\n";
last;
}
}
Run Code Online (Sandbox Code Playgroud)
在Python中:
# read_million.py
import gzip
filename = 'million_lines.txt.gz'
fh = gzip.open(filename)
for line in fh:
line = line.strip()
if line == '1000000':
print "This is the millionth line: Python"
break
Run Code Online (Sandbox Code Playgroud)
无论出于何种原因,Python脚本需要大约8倍的时间:
$ time perl read_million.pl ; time python read_million.py
This is the millionth line: Perl
real 0m0.329s
user 0m0.165s
sys 0m0.019s
This is the millionth line: Python
real 0m2.663s
user 0m2.154s
sys 0m0.074s
Run Code Online (Sandbox Code Playgroud)
我尝试分析这两个脚本,但实际上没有太多的代码可以分析.Python脚本大部分时间都花在for line in fh; Perl脚本大部分时间用在if($_ eq "1000000").
现在,我知道Perl和Python有一些预期的差异.例如,在Perl中,我使用subproc to UNIX gzip命令打开文件句柄; 在Python中,我使用gzip库.
我该怎么做才能加速这个脚本的Python实现(即使我从未达到Perl性能)?也许gzipPython中的模块很慢(或者我使用它的方式很糟糕); 有更好的解决方案吗?
以下是read_million.py逐行分析的样子.
Line # Hits Time Per Hit % Time Line Contents
==============================================================
2 @profile
3 def main():
4
5 1 1 1.0 0.0 filename = 'million_lines.txt.gz'
6 1 472 472.0 0.0 fh = gzip.open(filename)
7 1000000 5507042 5.5 84.3 for line in fh:
8 1000000 582653 0.6 8.9 line = line.strip()
9 1000000 443565 0.4 6.8 if line == '1000000':
10 1 25 25.0 0.0 print "This is the millionth line: Python"
11 1 0 0.0 0.0 break
Run Code Online (Sandbox Code Playgroud)
编辑#2:
我现在也subprocess根据@Kirk Strauser和其他人尝试了python模块.它更快:
Python"subproc"解决方案:
# read_million_subproc.py
import subprocess
filename = 'million_lines.txt.gz'
gzip = subprocess.Popen(['gzip', '-cdfq', filename], stdout=subprocess.PIPE)
for line in gzip.stdout:
line = line.strip()
if line == '1000000':
print "This is the millionth line: Python"
break
gzip.wait()
Run Code Online (Sandbox Code Playgroud)
这是我迄今为止尝试过的所有事情的对照表:
method average_running_time (s)
--------------------------------------------------
read_million.py 2.708
read_million_subproc.py 0.850
read_million.pl 0.393
Run Code Online (Sandbox Code Playgroud)
在测试了许多可能性后,看起来这里的罪魁祸首是:
gzip程序正在这样做(并且它是用C语言编写的,因此运行速度非常快); 在该版本的代码中,您将并行计算与串行计算进行比较.python与-E开关(禁用的检查PYTHON*启动环境变量)和-S开关(禁用自动import site,避免了很多动态的sys.path设置/操作涉及磁盘I/O以牺牲对任何非内置库的访问权为代价.subprocess模块比Perl的open调用高一点,并且用Python实现(在低级基元之上).通用subprocess代码加载时间更长(加剧启动时间问题)并增加了流程启动本身的开销.subprocess默认为无缓冲I/O,因此除非传递显式bufsize参数,否则执行更多系统调用(4096到8192似乎工作正常)line.strip()电话涉及的开销比你想象的要多; 函数和方法调用在Python中比实际应该更昂贵,并且line.strip()不会str像Perl chomp那样改变原因(因为Python str是不可变的,而Perl字符串是可变的)几个版本的代码将绕过大多数这些问题.首先,优化subprocess:
#!/usr/bin/env python
import subprocess
# Launch with subprocess in list mode (no shell involved) and
# use a meaningful buffer size to minimize system calls
proc = subprocess.Popen(['gzip', '-cdfq', 'million_lines.txt.gz'], stdout=subprocess.PIPE, bufsize=4096)
# Iterate stdout directly
for line in proc.stdout:
if line == '1000000\n': # Avoid stripping
print("This is the millionth line: Python")
break
# Prevent deadlocks by terminating, not waiting, child process
proc.terminate()
Run Code Online (Sandbox Code Playgroud)
第二,纯Python,主要是基于内置(C级)API的代码(它消除了大多数无关的启动开销,并且表明Python的gzip模块与gzip程序没有明显的区别),以可读性/可维护性/简洁性为代价进行了微观优化.可移植性:
#!/usr/bin/env python
import os
rpipe, wpipe = os.pipe()
def reader():
import gzip
FILE = "million_lines.txt.gz"
os.close(rpipe)
with gzip.open(FILE) as inf, os.fdopen(wpipe, 'wb') as outf:
buf = bytearray(16384) # Reusable buffer to minimize allocator overhead
while 1:
cnt = inf.readinto(buf)
if not cnt: break
outf.write(buf[:cnt] if cnt != 16384 else buf)
pid = os.fork()
if not pid:
try:
reader()
finally:
os._exit()
try:
os.close(wpipe)
with os.fdopen(rpipe, 'rb') as f:
for line in f:
if line == b'1000000\n':
print("This is the millionth line: Python")
break
finally:
os.kill(pid, 9)
Run Code Online (Sandbox Code Playgroud)
在我的本地系统上,在最好的六次运行中,subprocess代码采用:
0.173s/0.157s/0.031s wall/user/sys time.
Run Code Online (Sandbox Code Playgroud)
基于Python代码的原语没有外部实用程序,可以达到以下最佳时间:
0.147s/0.103s/0.013s
Run Code Online (Sandbox Code Playgroud)
(虽然这是一个异常值;一个好的挂钟时间通常更像是0.165).-E -S通过消除设置导入机器以处理非内置函数的开销,添加到调用会削减另一个0.01-0.015秒的挂钟和用户时间; 在其他评论中,你提到你的Python需要将近0.6秒的时间才能完全执行任何操作(但其他方式似乎与我的相似),这可能表明你对非默认包或环境的方式有了更多的了解定制正在进行,-E -S可能会为您节省更多.
Perl代码,不修改你给我的东西(除了使用3+ arg open去除字符串解析并在退出之前将pid返回的内容存储open到显式kill它)有一个最佳时间:
0.183s/0.216s/0.005s
Run Code Online (Sandbox Code Playgroud)
无论如何,我们谈论的是微不足道的差异(对于挂钟和用户时间,从运行到运行的时间抖动大约是0.025秒,因此Python在挂钟时间上的胜利大多是微不足道的,尽管它确实有效地节省了用户时间).与Perl一样,Python可以获胜,但非语言相关的问题更为重要.