0

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

Python慢​​读性能问题

感谢您迄今为止的所有评论,非常有用

我有大约 40M 的 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 time 都会给我这样的结果:

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";
    }
}
4

2 回答 2

4

根据您的 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

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

0:00:00.009327
0:00:00.000338

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

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

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

正如评论中所建议的那样,使用迭代器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. 以防万一您不知道,默认情况下,多处理池只会启动与 CPU 内核数量一样多的工作程序。如果您已经知道,请忽略此评论。为您的池指定 12 个进程对我来说似乎有点奇怪。
于 2014-10-02T23:19:45.437 回答
1

编辑

忘记感谢此线程的贡献者:

Python慢​​读性能问题

谁帮我解决了这个问题。

编辑

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

基本上 Perl 默认按字典顺序排序(即 1,11,2,22),Python 按目录顺序(ls -U)排序,文件是按自然顺序(1,2,3,4)创建的,所以我取了原始文件Python slurp 并在搜索 Stackoverflow 以获得简单的自然排序后创建了一个 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 以在处理之前对目录内容进行排序。

感谢所有的帮助,老实说,我不认为读取文件的顺序会有这么大的不同!

于 2014-10-06T11:22:59.343 回答