0

これを回答済みとしてマークし、速度の問題が実際に発生していると思われる場所に関するより簡単なトピックを開始しました

Python の読み取りパフォーマンスが遅い問題

これまでのすべてのコメントに感謝します。非常に便利です

約 4,000 万個の XML ファイルが (均等ではなく) 約 2 か所に分散しています。60K のサブディレクトリ。構造は 10 桁の番号分割に基づいているため、次のようになります。

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

単一のフィールドの値を引き出して値とファイル名を出力するファイルに対して実行する perl スクリプトがあります。Perl スクリプトは、深さ 2 のすべてのディレクトリのリスト全体で最大 12 の並列インスタンスを実行する bash スクリプトにラップされ、次に各ディレクトリをたどって最下層でファイルを見つけたら処理します。

複数の実行からディスク キャッシュを取得すると、プロセスの 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 が問題を引き起こしているように見えるため、perl スクリプトよりも python スクリプトの方がはるかに効率が悪いということです。

うまくいけば、それで十分な背景があります。私の質問は、アイデアが不足しているため、愚かなことをしているのか、トリックを見逃しているのかということですが、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を使用して時間を節約できる場合があります。現在、1 つのエントリだけに関心がある場合でも、ファイル全体を読み込んでいます。データがファイルの先頭近くに発生する傾向がある場合は、実際にファイルを読み込む代わりに、ファイルをメモリにマップし、既存とまったく同じように正規表現検索を実行して、それで完了できます。

次に、2 つの方法の比較を示します。

1,000,000 行の「tmp_large.txt」というテキスト ファイルがあります。各行には小文字のアルファベットがあります。ファイルのほぼ半分の 1 行で、文字 '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

3 分の 1 弱の実行時間の節約が見られます。ただし、探している文字列を入力ファイルの先頭に配置すると、次の結果が表示されます。

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()

次に、50,000 個のドキュメントに対して 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 に答える