Python vs Perl:性能读取gzip压缩文件

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中的模块很慢(或者我使用它的方式很糟糕); 有更好的解决方案吗?

编辑#1

以下是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)

Sha*_*ger 7

在测试了许多可能性后,看起来这里的罪魁祸首是:

  1. 将苹果与橙子进行比较:在您的原始测试用例中,Perl没有进行文件I/O或解压缩工作,gzip程序正在这样做(并且它是用C语言编写的,因此运行速度非常快); 在该版本的代码中,您将并行计算与串行计算进行比较.
  2. 口译员启动时间; 在绝大多数系统上,Python开始运行需要更长的时间(我相信因为启动时会加载更多文件).我的机器上的解释器启动时间大约是总挂钟时间的一半,用户时间的30%,以及大部分系统时间.在Python中完成的实际工作被启动时间所淹没,因此您的基准测试与将启动时间作为比较执行工作所需的时间相同.后来另外:可以减少从Python的开销进一步启动位通过调用python-E开关(禁用的检查PYTHON*启动环境变量)和-S开关(禁用自动import site,避免了很多动态的sys.path设置/操作涉及磁盘I/O以牺牲对任何非内置库的访问权为代价.
  3. Python的subprocess模块比Perl的open调用高一点,并且用Python实现(在低级基元之上).通用subprocess代码加载时间更长(加剧启动时间问题)并增加了流程启动本身的开销.
  4. Python 2 subprocess默认为无缓冲I/O,因此除非传递显式bufsize参数,否则执行更多系统调用(4096到8192似乎工作正常)
  5. 这个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可以获胜,但非语言相关的问题更为重要.