web-dev-qa-db-ja.com

複数のファイルを同時に読み取るのが、連続して読み取るよりも遅いのはなぜですか?

ディレクトリで見つかった多くのファイルを解析しようとしていますが、マルチプロセッシングを使用するとプログラムが遅くなります。

# Calling my parsing function from Client.
L = getParsedFiles('/home/tony/Lab/slicedFiles') <--- 1000 .txt files found here.
                                                       combined ~100MB

pythonドキュメントからのこの例に従ってください:

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    p = Pool(5)
    print(p.map(f, [1, 2, 3]))

私はこのコードを書きました:

from multiprocessing import Pool
from api.ttypes import *

import gc
import os

def _parse(pathToFile):
    myList = []
    with open(pathToFile) as f:
        for line in f:
            s = line.split()
            x, y = [int(v) for v in s]
            obj = CoresetPoint(x, y)
            gc.disable()
            myList.append(obj)
            gc.enable()
    return Points(myList)

def getParsedFiles(pathToFile):
    myList = []
    p = Pool(2)
    for filename in os.listdir(pathToFile):
        if filename.endswith(".txt"):
            myList.append(filename)
    return p.map(_pars, , myList)

私は例に従い、.txtで終わるすべてのファイルの名前をリストに入れてから、プールを作成し、それらを関数にマッピングしました。次に、オブジェクトのリストを返します。各オブジェクトは、ファイルの解析済みデータを保持します。しかし、次の結果が得られたことには驚きます。

#Pool 32  ---> ~162(s)
#Pool 16 ---> ~150(s)
#Pool 12 ---> ~142(s)
#Pool 2 ---> ~130(s)

グラフ:
enter image description here

機械仕様:

62.8 GiB RAM
Intel® Core™ i7-6850K CPU @ 3.60GHz × 12   

ここで何が欠けていますか?
前もって感謝します!

15
Tony Tannous

あなたは I/Oバウンド のようです:

コンピュータサイエンスでは、I/Oバウンドとは、計算の完了に要する時間が、主に入出力操作の完了を待機するのに費やされた時間によって決定される状態を指します。これは、CPUバウンドであるタスクの反対です。この状況は、データが要求される速度が消費される速度よりも遅い場合、つまりデータの処理よりもデータの要求により多くの時間がかかる場合に発生します。

サブプロセスが使用可能になったときに、メインスレッドで読み取りを実行し、データをプールに追加する必要があるでしょう。これは、mapを使用する場合とは異なります。

一度に1行ずつ処理していて、入力が分割されているため、 fileinput を使用できます。複数のファイルの行を反復処理し、ファイルではなく関数処理行にマップするには、次のようにします。

一度に1行ずつ渡すのは遅すぎる可能性があるため、マップにチャンクを渡すように要求し、スイートスポットが見つかるまで調整できます。私たちの関数は行のチャンクを解析します:

def _parse_coreset_points(lines):
    return Points([_parse_coreset_point(line) for line in lines])

def _parse_coreset_point(line):
    s = line.split()
    x, y = [int(v) for v in s]
    return CoresetPoint(x, y)

そして私たちの主な機能:

import fileinput

def getParsedFiles(directory):
    pool = Pool(2)

    txts = [filename for filename in os.listdir(directory):
            if filename.endswith(".txt")]

    return pool.imap(_parse_coreset_points, fileinput.input(txts), chunksize=100)
11
Peter Wood

すべてのスイッチがハードディスクの読み取りヘッドを配置するために約10ミリ秒の余分な遅延を引き起こすため(SSDでは異なる場合があります)、一般に、異なるスレッドから同じ物理(回転)ハードディスクから同時に読み取ることはお勧めできません。 。

@ peter-woodがすでに述べたように、1つのスレッドでデータを読み取り、他のスレッドでそのデータを処理することをお勧めします。

また、実際に違いをテストするには、いくつかの大きなファイルを使用してテストを行う必要があると思います。例:現在のハードディスクは、約100MB /秒を読み取ることができるはずです。したがって、100kBファイルのデータを一度に読み取るのに1msかかりますが、読み取りヘッドをそのファイルの先頭に配置すると10msかかります。

一方、数値を見ると(それらが単一ループの場合)、I/Oバウンドであることがここでの唯一の問題であるとは信じがたいです。合計データは100MBで、ディスクからの読み取りに1秒とオーバーヘッドが必要ですが、プログラムには130秒かかります。その数がディスク上のコールドファイルに関するものか、またはデータがOSによって既にキャッシュされている複数のテストの平均(62 GBまたはRAM 2回目にキャッシュされる)-両方の数値を確認することは興味深いでしょう。

だから何か他にある必要があります。ループを詳しく見てみましょう:

_for line in f:
    s = line.split()
    x, y = [int(v) for v in s]
    obj = CoresetPoint(x, y)
    gc.disable()
    myList.append(obj)
    gc.enable()
_

私はPythonを知りませんが、ここではgc呼び出しが問題だと思います。これらは、ディスクから読み取られるすべての行で呼び出されます。それらの呼び出しがどれほど高価であるか(またはgc.enable()がガベージコレクションをトリガーする場合など)、なぜappend(obj)のみで必要になるのかはわかりませんが、他の問題がある可能性がありますこれはマルチスレッドなので:

gcオブジェクトがグローバル(つまり、スレッドローカルではない)であるとすると、次のようになります。

_thread 1 : gc.disable()
# switch to thread 2
thread 2 : gc.disable()
thread 2 : myList.append(obj)
thread 2 : gc.enable()
# gc now enabled!
# switch back to thread 1 (or one of the other threads)
thread 1 : myList.append(obj)
thread 1 : gc.enable()
_

スレッドの数<=コアの数の場合、切り替えも行われず、すべてが同時に呼び出されます。

また、gcオブジェクトがスレッドセーフである場合(そうでない場合はさらに悪くなります)、内部状態を安全に変更するためにロックを行う必要があり、他のすべてのスレッドを強制的に待機させます。

たとえば、gc.disable()は次のようになります。

_def disable()
    lock()  # all other threads are blocked for gc calls now
    alter internal data
    unlock()
_

また、gc.disable()gc.enable()はタイトなループで呼び出されるため、複数のスレッドを使用するとパフォーマンスが低下します。

したがって、これらの呼び出しを削除するか、本当に必要な場合はプログラムの最初と最後に配置することをお勧めします(または、最初にgcを無効にするだけで、gcを実行する必要はありません。プログラムを終了する直前)。

Python=オブジェクトをコピーまたは移動する方法によっては、myList.append(CoresetPoint(x, y))を使用する方が少し良い場合もあります。

したがって、gc呼び出しなしで、1つのスレッドを使用して1つの100MBファイルで同じことをテストすることは興味深いでしょう。

処理に読み取りよりも時間がかかる場合(つまり、I/Oバインドではない場合)、1つのスレッドを使用してバッファー内のデータを読み取り(まだキャッシュされていない場合は、1つの100MBファイルで1秒または2秒かかる)、複数のスレッドがデータ(ただし、これらのgc呼び出しはタイトループにありません)。

スレッドを使用できるようにするために、データを複数のファイルに分割する必要はありません。同じファイルの異なる部分を処理させるだけです(14GBファイルでも)。

6
Danny_ds