【问题标题】:Python multiprocessing text extraction performance issue vs Perl equivalentPython 多处理文本提取性能问题与 Perl 等效
【发布时间】:2014-10-01 22:27:51
【问题描述】:

将此标记为已回答并围绕速度问题真正出现的地方开始一个更简单的主题

Python slow read performance issue

感谢迄今为止所有的 cmets,非常有用

我有大约 4000 万个 XML 文件(不均匀)分布在大约 400 万个 XML 文件中。 60K 子目录,结构基于 10 位数字拆分,所以:

12/34/56/78/90/files.xml

我有一个 perl 脚本,它针对文件运行,提取单个字段的值并打印值和文件名。 Perl 脚本封装在一个 bash 脚本中,该脚本在深度 2 的所有目录列表中运行最多 12 个并行实例,然后向下遍历每个实例,并在找到它们时在底层处理文件。

从多个运行中取出磁盘缓存,进程的 unix 时间返回大约:

real    37m47.993s
user    49m50.143s
sys     54m57.570s

我想将它迁移到 python 脚本(作为学习练习和测试),因此创建了以下内容(在大量阅读各种 python 方法之后):

    import glob, os, re
    from multiprocessing import Pool

    regex = re.compile(r'<field name="FIELDNAME">([^<]+)<', re.S)

    def extractField(root, dataFile):
            line = ''
            filesGlob = root + '/*.xml'
            global regex
            for file in glob.glob(filesGlob):
                    with open(file) as x:
                            f = x.read()
                    match = regex.search(f)
                    line += file + '\t' + match.group(1) + '\n'

            dataFile.write(line)

    def processDir(top):
            topName = top.replace("/", "")
            dataFile = open('data/' + topName + '.data', 'w')
            extractField(top, dataFile)
            dataFile.close()

    filesDepth5 = glob.glob('??/??/??/??/??')
    dirsDepth5 = filter(lambda f: os.path.isdir(f), filesDepth5)
    processPool = Pool(12)
    processPool.map(processDir, dirsDepth5)
    processPool.close()
    processPool.join()

但是无论我如何在运行时对内容进行切片,unix 时间都会给我这样的结果:

real    131m48.731s
user    35m37.102s
sys     48m11.797s

如果我在单个线程中针对一小部分子集(最终被完全缓存)运行 python 和 perl 脚本,因此没有磁盘 io(根据 iotop),那么脚本运行的时间几乎相同。

目前我能想到的唯一结论是,文件 io 在 python 脚本中的效率远低于在 perl 脚本中的效率,因为似乎是 io 导致了问题。

所以希望这是足够的背景知识,我的问题是我是在做一些愚蠢的事情还是错过了一个技巧,因为我已经没有想法了,但无法相信 io 会导致处理时间出现如此大的差异。

感谢任何指针,并将根据需要提供更多信息。

谢谢

Perl 脚本参考如下:

use File::Find;

my $cwd = `pwd`;
chomp $cwd;
find( \&hasxml, shift );

sub hasxml {
    if (-d) {
        my @files = <$_/*.xml>;
        if ( scalar(@files) > 0 ) {
            process("$cwd/${File::Find::dir}/$_");
        }
    }
}

sub process {
    my $dir = shift;

    my @files = <$dir/*.xml>;

    foreach my $file (@files) {
        my $fh;
        open( $fh, "< $file" ) or die "Could not read file <$file>";
        my $contents = do { local $/; <$fh> };
        close($fh);
        my ($id) = $contents =~ /<field name="FIELDNAME">([^<]+)<\/field>/s;
        print "$file\t<$id>\n";
    }
}

【问题讨论】:

  • 也许这会对你有所帮助:stackoverflow.com/questions/14863224/… 我认为你应该分析你的应用程序,看看它在哪里花费的时间最多,否则它有点猜测......
  • 我会看看谢谢,啊应该已经添加了我正在阅读的 XML 文件,每个文件的大小最多为 4K,但我会尝试提到的缓冲来查看。我尝试进行分析,但是当我运行 - python -m cProfile script.py it barfs 时,我怀疑它不知道如何处理多处理元素,但我承认我没有详细查看错误。
  • user/sys 更低(暗示 python 更快)但 real 更高(暗示我们没有得到并行化)......父/子协议很重要。试试processPool.map(processDir, dirsDepth5, chunksize=16),它会以更大的批量发送作业,看看是否会有所不同。
  • 有趣的是没有看到那个参数,将尝试一下如何分析多进程脚本已经阅读了一些关于如何在代码中而不是从命令行中执行它的帖子我读过多进程脚本不适合!
  • 另一个想法, map() 建立了一个结果列表——在你的例子中是 4000 万。试试for _ in processPool.imap_unordered(processDir, dirsDepth5, chunksize=16): pass

标签: python perl python-multiprocessing


【解决方案1】:

根据您的 XML 文件的结构,您可以使用mmap 节省一些时间。目前,您正在阅读整个文件,即使您只对一个条目感兴趣。如果您的数据往往出现在文件顶部附近,您可以将文件映射到内存而不是实际读取它,执行与您已经完全相同的正则表达式搜索,然后完成它。

以下是两种方法的比较:

我有一个名为“tmp_large.txt”的文本文件,其中有 1,000,000 行。每行都有小写字母。在文件大约一半的一行中,我将字母“m”替换为“x”,并且正在搜索该字符串:

import re
import mmap

from timeit import timeit
from datetime import timedelta

c_regex = re.compile('defghijklxnopqrstuvwx')

def read_file():
    with open('tmp_large.txt', 'r') as fi:
        f = fi.read()
        match = c_regex.search(f)

def mmap_file():
    with open('tmp_large.txt', 'r+b') as fi: # must open as binary for mmap
        mm = mmap.mmap(fi.fileno(), 0)
        match = c_regex.search(mm)
        mm.close()

t1 = timedelta(seconds=timeit(read_file, setup='gc.enable()', number=1))
t2 = timedelta(seconds=timeit(mmap_file, setup='gc.enable()', number=1))

print(t1)
print(t2)

这个场景产生这个输出:

0:00:00.036021
0:00:00.028974

我们看到执行时间节省不到三分之一。但是,如果我将要查找的字符串放在输入文件的 top 处,我们会看到以下结果:

0:00:00.009327
0:00:00.000338

显然这两种方法都更快,但对于内存映射方法而言,节省的时间要大得多。

由于我不知道您的数据结构或您的文件有多大,因此您可能会看到不太显着的结果。但是只要您要查找的数据不在目标文件的末尾,您可能会看到内存映射文件的一些改进,因为它可以避免将数据带入您没有的内存实际上最终使用。

作为旁注,我还尝试迭代文件中的行,直到我们找到与正则表达式匹配的行,但它太慢了,无法在这里包含。此外,我确实确认了正则表达式在我的示例中实际上是匹配的,但为了简洁起见,我删除了打印代码和结果

按照 cmets 中的建议,使用迭代器 iglob 并将 map 替换为 apply_async 之类的东西也可能有助于加快速度,因为它们都有助于减少内存占用:

processPool = Pool(12)

for dir_or_file in glob.iglob('??/??/??/??/??'):
    if os.path.isdir(dir_or_file):
        processPool.apply_async(processDir, (dir_or_file,))

processPool.close()
processPool.join()

这种方法还允许您的子流程开始处理第一个文件,而您仍在识别其余文件。

其他一些代码说明:

  1. 您不需要正则表达式上的 re.S 标志,因为您实际上没有任何 '.'在正则表达式模式中。
  2. 除非您有一些令人信服的理由不这样做,否则您应该使用 with open() 构造打开输出文件,就像打开输入文件一样,以防止出现异常时打开的文件描述符杂乱无章。
  3. 在计算 dataFile 和 filesGlob 时,请考虑使用 os.path.join() 而不是手动添加路径分隔符。从长远来看,它更不容易出错。
  4. 您不需要global regex 行。就像我的示例一样,您始终可以在没有它的情况下读取和调用全局对象的方法。只有在要修改全局时才需要它。
  5. 以防万一您不知道,mutliprocessing pools 默认情况下只会启动与 CPU 内核数量一样多的工作程序。如果您已经知道,请忽略此评论。为您的池指定 12 个进程对我来说有点奇怪。

【讨论】:

  • 这是一些很棒的信息,谢谢,我已经完成了你提到的事情,它已经有所改善,但似乎磁盘读取速度仍然是一个问题,因为即使 Perl 版本大大超过了执行Python 一个,在一组大约 250k 的文件上总共大约 1GB,python 花了 35s 和 Perl 17s。我已经开始了一个关于这个 stackoverflow.com/questions/26178038/… 的新问题。感谢您的建议在此过程中教会了我很多东西:-)
  • @Simon:您可以使用 iglob()for result in pool.imap_unordered(process_dir, (d for d in iglob(...) if isdir(d)), chunksize=1000): ... 来避免一次将所有文件排队。确保在process_dir() 中捕获并记录(或返回)所有异常。
  • @Simon:据我所知mmap() 不会提高 Linux 的性能,但可能会提高 Windows 的性能。但是mmap() 很方便,如果您需要将潜在的大文件视为一个字节串,就像@SLawson 使用正则表达式显示的那样。如果是 xml 文件,你可以使用 etree.iterparse()avoid using regexes to parse xml/html :)
  • @J.F.Sebastian mmap 当然可以在 Linux 上提供超过 file.read() 的速度改进,如上述测试所示(我使用的是 RHEL 6)。事实上,我只是将目标行放在文件的最后一行再次运行它,内存映射仍然始终(非常)略快于file.read()
  • @SLawson:我认为这是一个基准工件。见map vs reading blocks
【解决方案2】:

编辑

忘了感谢这个帖子的贡献者:

Python slow read performance issue

谁帮我解决了这个问题。

编辑

最终归结为目录读取的顺序,这适用于我的主应用程序以及测试。

Perl 默认按字典顺序(即 1,11,2,22)排序,Python 按目录顺序(ls -U)排序,文件按自然顺序(1,2,3,4)创建,所以我在搜索 Stackoverflow 以获得简单的自然排序之后,采用原始 Python slurp 并创建了 slurpNatural:

import glob, sys, re

def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
    return [int(text) if text.isdigit() else text.lower()
            for text in re.split(_nsre, s)]

for file in sorted(glob.iglob(sys.argv[1] + '/*.xml'), key=natural_sort_key):
    with open(file) as x:
        f = x.read()

然后我针对 50K 文档运行了所有 3 个文档并得到:

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ /usr/bin/time perl slurp.pl 1
1.21user 2.17system 0:12.70elapsed 26%CPU (0avgtext+0avgdata 9140maxresident)k
1234192inputs+0outputs (22major+2466minor)pagefaults 0swaps

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ /usr/bin/time python slurp.py 1
2.88user 6.13system 4:48.00elapsed 3%CPU (0avgtext+0avgdata 8020maxresident)k
1236304inputs+0outputs (35major+52252minor)pagefaults 0swaps

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ /usr/bin/time python slurpNatural.py 1
1.25user 2.82system 0:10.70elapsed 38%CPU (0avgtext+0avgdata 22408maxresident)k
1237360inputs+0outputs (35major+56531minor)pagefaults 0swaps

反映创建顺序的自然排序显然是最快的,在这种情况下反映了我的实际数据是如何创建的,因此现在更改了 Python 以在处理之前对目录内容进行排序。

感谢大家的帮助,老实说,我没想到读取文件的顺序会有这么大的不同!

【讨论】:

  • 我知道这个答案已经存在了一段时间,但我现在才注意到它。我认为您应该考虑接受自己的答案而不是我的答案。看起来它在解决您问题中的真正问题方面做得更好。我肯定从阅读中学到了一些新东西,并且认为其他人也会。接受它会将它推到顶部以保证它被看到(现在可能不会,因为我的被接受并且有更多的选票)。
猜你喜欢
  • 2022-10-19
  • 2012-09-29
  • 2013-07-07
  • 2023-03-19
  • 1970-01-01
  • 2011-04-15
  • 1970-01-01
  • 2012-11-02
  • 1970-01-01
相关资源
最近更新 更多