web-dev-qa-db-ja.com

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

以前のスレッドに続いて、PerlスクリプトからPythonスクリプトに移行する際に問題を骨抜きにしたので、Pythonでファイルを丸呑みするときに大きなパフォーマンスの問題が見つかりました。これをUbuntuサーバーで実行します。

注意:これはX対Yのスレッドではありません。これがどのようなものなのか、あるいは私が愚かなことをしているのかどうかを根本的に知る必要があります。

テストデータとして50,000個の10kbファイルを作成しました(これは、処理しているものの平均ファイルサイズを反映しています)。

mkdir 1
cd 1
for i in {1..50000}; do dd if=/dev/zero of=$i.xml bs=1 count=10000; done
cd ..
cp -r 1 2

できるだけ簡単に2つのスクリプトを作成しました。

Perl

foreach my $file (<$ARGV[0]/*.xml>){
    my $fh;
    open($fh, "< $file");
    my $contents = do { local $/; <$fh> };
    close($fh);
}

Python

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

次に、キャッシュをクリアして2つのSlurpスクリプトを実行しました。実行するたびに、次のコマンドを使用してキャッシュを再度クリーンアップしました。

sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

次に、毎回ディスクからすべてを読み取っていることを確認するために監視されました。

Sudo iotop -a -u me

RAID 10ディスクを備えた物理マシンと、VMがRAID 1 SSDにある新しいVMセットアップでこれを試しました。VMからのテスト実行が含まれています。物理サーバーはほとんど同じで、高速でした。

$ time python readFiles.py 1
    real    5m2.493s
    user    0m1.783s
    sys     0m5.013s

$ time Perl readFiles.pl 2
    real    0m13.059s
    user    0m1.690s
    sys     0m2.471s

$ time Perl readFiles.pl 2
    real    0m13.313s
    user    0m1.670s
    sys     0m2.579s

$ time python readFiles.py 1
    real    4m43.378s
    user    0m1.772s
    sys     0m4.731s

PerlがDISK READを実行しているときのiotopで、Python DISK READが2M/sでIOWAIT 97%の場合、約45 M/s、IOWAITが約70%であることに気付きました。私がそれらを煮詰めてできる限り単純なものにしたので、ここからどこへ行くべきかわかりません。

関連する場合

$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2

$ Perl -v
This is Perl 5, version 18, Subversion 2 (v5.18.2) built for x86_64-linux-gnu-thread-multi

必要に応じて詳細情報

私はstraceを実行してファイル1000.xmlの情報を取得しましたが、すべて同じことをしているようです:

Perl

$strace -f -T -o trace.Perl.1 Perl readFiles.pl 2

32303 open("2/1000.xml", O_RDONLY)      = 3 <0.000020>
32303 ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7fff7f6f7b90) = -1 ENOTTY (Inappropriate ioctl for device) <0.000016>
32303 lseek(3, 0, SEEK_CUR)             = 0 <0.000016>
32303 fstat(3, {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000016>
32303 fcntl(3, F_SETFD, FD_CLOEXEC)     = 0 <0.000017>
32303 fstat(3, {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000030>
32303 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 8192) = 8192 <0.005323>
32303 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 8192) = 1808 <0.000022>
32303 read(3, "", 8192)                 = 0 <0.000019>
32303 close(3)                          = 0 <0.000017>

Python

$strace -f -T -o trace.python.1 python readFiles.py 1

32313 open("1/1000.xml", O_RDONLY)      = 3 <0.000021>
32313 fstat(3, {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000017>
32313 fstat(3, {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000019>
32313 lseek(3, 0, SEEK_CUR)             = 0 <0.000018>
32313 fstat(3, {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000018>
32313 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa18820a000 <0.000019>
32313 lseek(3, 0, SEEK_CUR)             = 0 <0.000018>
32313 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 8192) = 8192 <0.006795>
32313 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 1808 <0.000031>
32313 read(3, "", 4096)                 = 0 <0.000018>
32313 close(3)                          = 0 <0.000027>
32313 munmap(0x7fa18820a000, 4096)      = 0 <0.000022>

私が気付いた1つの違いは、関連があるかどうかは不明ですが、Perlがすべてのファイルに対してこれを実行するように見えますが、pythonはそうではありません。

32303 lstat("2/1000.xml", {st_mode=S_IFREG|0664, st_size=10000, ...}) = 0 <0.000022>

また、-cを使用してstraceを実行しました(上位の呼び出しをいくつか受けただけです)。

Perl

$ time strace -f -c Perl readFiles.pl 2
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 44.07    3.501471          23    150018           read
 12.54    0.996490          10    100011           fstat
  9.47    0.752552          15     50000           lstat
  7.99    0.634904          13     50016           open
  6.89    0.547016          11     50017           close
  6.19    0.491944          10     50008     50005 ioctl
  6.12    0.486208          10     50014         3 lseek
  6.10    0.484374          10     50001           fcntl

real    0m37.829s
user    0m6.373s
sys     0m25.042s

Python

$ time strace -f -c python readFiles.py 1
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 42.97    4.186173          28    150104           read
 15.58    1.518304          10    150103           fstat
 10.51    1.023681          20     50242       174 open
 10.12    0.986350          10    100003           lseek
  7.69    0.749387          15     50047           munmap
  6.85    0.667576          13     50071           close
  5.90    0.574888          11     50073           mmap

real    5m5.237s
user    0m7.278s
sys     0m30.736s

-Tをオンにしてstrace出力を解析し、各ファイルの最初の8192バイトの読み取りをカウントしました。これが時間が経過していることは明らかです。以下は、ファイルの50000回の最初の読み取りに費やされた合計時間とそれに続く各読み取りの平均時間。

300.247128000002 (0.00600446220302379)   - Python
11.6845620000003 (0.000233681892724297)  - Perl

それが役立つかどうかわかりません!

PDATE 2 os.openとos.readを使用するようにPythonのコードを更新し、最初の4096バイトの単一の読み取りを実行します(必要な情報が上部にあるため、私にとってはうまくいきます)ファイルの)、strace内の他のすべての呼び出しも排除します:

18346 open("1/1000.xml", O_RDONLY)      = 3 <0.000026>
18346 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096 <0.007206>
18346 close(3)                          = 0 <0.000024>

$ time strace -f -c python readFiles.py 1
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 55.39    2.388932          48     50104           read
 22.86    0.986096          20     50242       174 open
 20.72    0.893579          18     50071           close

real    4m48.751s
user    0m3.078s
sys     0m12.360s

Total Time (avg read call)
282.28626 (0.00564290374812595)

まだ良くありません...次に、AzureでVMを作成して、別の例を試してみます。

PDATE 3-このサイズの謝罪!!

3つのセットアップで(@ J.F.Sebastian)スクリプトを使用して興味深い結果をいくつかわかりました。簡潔にするために、最初に出力を取り除き、キャッシュから超高速で実行されるすべてのテストを削除しました。

0.23user 0.26system 0:00.50elapsed 99%CPU (0avgtext+0avgdata 9140maxresident)k
0inputs+0outputs (0major+2479minor)pagefaults 0swaps

Azure A2標準VM(2コア3.5GB RAMディスクが不明ですが遅い)

$ uname -a
Linux servername 3.13.0-35-generic #62-Ubuntu SMP Fri Aug 15 01:58:42 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
$ Perl -v
This is Perl 5, version 18, Subversion 2 (v5.18.2) built for x86_64-linux-gnu-thread-multi
(with 41 registered patches, see Perl -V for more detail)

+ /usr/bin/time Perl Slurp.pl 1
1.81user 2.95system 3:11.28elapsed 2%CPU (0avgtext+0avgdata 9144maxresident)k
1233840inputs+0outputs (20major+2461minor)pagefaults 0swaps
+ clearcache
+ sync
+ Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
+ /usr/bin/time python Slurp.py 1
1.56user 3.76system 3:06.05elapsed 2%CPU (0avgtext+0avgdata 8024maxresident)k
1232232inputs+0outputs (14major+52273minor)pagefaults 0swaps
+ /usr/bin/time Perl Slurp.pl 2
1.90user 3.11system 6:02.17elapsed 1%CPU (0avgtext+0avgdata 9144maxresident)k
1233776inputs+0outputs (16major+2465minor)pagefaults 0swaps

両方の比較可能な最初のスラープ結果、2番目のPerlスラープ中に何が起こっていたのかわかりませんか?

My VMWare Linux VM(2コア8GB RAMディスクRAID1 SSD)

$ uname -a
Linux servername 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
$ Perl -v
This is Perl 5, version 18, Subversion 2 (v5.18.2) built for x86_64-linux-gnu-thread-multi
(with 41 registered patches, see Perl -V for more detail)

+ /usr/bin/time Perl Slurp.pl 1
1.66user 2.55system 0:13.28elapsed 31%CPU (0avgtext+0avgdata 9136maxresident)k
1233152inputs+0outputs (20major+2460minor)pagefaults 0swaps
+ clearcache
+ sync
+ Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
+ /usr/bin/time python Slurp.py 1
2.10user 4.67system 4:45.65elapsed 2%CPU (0avgtext+0avgdata 8012maxresident)k
1232056inputs+0outputs (14major+52269minor)pagefaults 0swaps
+ /usr/bin/time Perl Slurp.pl 2
2.13user 4.11system 5:01.40elapsed 2%CPU (0avgtext+0avgdata 9140maxresident)k
1233264inputs+0outputs (16major+2463minor)pagefaults 0swaps

今回は、以前と同様に、Perlは最初のSlurpの方がはるかに高速です。2番目のPerl Slurpで何が起こっているかはわかりませんが、この動作は以前には見られませんでした。 measure.shを再度実行したところ、結果はまったく同じでしたか、数秒かかりました。次に、通常の人が行うことと同じことを行い、Azureマシン3.13.0-35-genericに一致するようにカーネルを更新して、再びmeasure.shを実行しましたが、結果に違いはありませんでした。

好奇心から、それから、measure.shの1と2のパラメーターを入れ替えると、何か奇妙なことが起こりました。Perlが遅くなり、Pythonが高速になりました!

+ /usr/bin/time Perl Slurp.pl 2
1.78user 3.46system 4:43.90elapsed 1%CPU (0avgtext+0avgdata 9140maxresident)k
1234952inputs+0outputs (21major+2458minor)pagefaults 0swaps
+ clearcache
+ sync
+ Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
+ /usr/bin/time python Slurp.py 2
1.19user 3.09system 0:10.67elapsed 40%CPU (0avgtext+0avgdata 8012maxresident)k
1233632inputs+0outputs (14major+52269minor)pagefaults 0swaps
+ /usr/bin/time Perl Slurp.pl 1
1.36user 2.32system 0:13.40elapsed 27%CPU (0avgtext+0avgdata 9136maxresident)k
1232032inputs+0outputs (17major+2465minor)pagefaults 0swaps

これは私をさらに混乱させました:

---(物理サーバー(32コア132 GB RAMディスクRAID10 SAS)

$ uname -a
Linux servername 3.5.0-23-generic #35~precise1-Ubuntu SMP Fri Jan 25 17:13:26 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
$ python
Python 2.7.3 (default, Aug  1 2012, 05:14:39)
[GCC 4.6.3] on linux2
$ Perl -v
This is Perl 5, version 14, Subversion 2 (v5.14.2) built for x86_64-linux-gnu-thread-multi
(with 55 registered patches, see Perl -V for more detail)

+ /usr/bin/time Perl Slurp.pl 1
2.22user 2.60system 0:15.78elapsed 30%CPU (0avgtext+0avgdata 43728maxresident)k
1233264inputs+0outputs (15major+2984minor)pagefaults 0swaps
+ clearcache
+ sync
+ Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
+ /usr/bin/time python Slurp.py 1
2.51user 4.79system 1:58.53elapsed 6%CPU (0avgtext+0avgdata 34256maxresident)k
1234752inputs+0outputs (16major+52385minor)pagefaults 0swaps
+ /usr/bin/time Perl Slurp.pl 2
2.17user 2.95system 0:06.96elapsed 73%CPU (0avgtext+0avgdata 43744maxresident)k
1232008inputs+0outputs (14major+2987minor)pagefaults 0swaps

ここではPerlが毎回勝つようです。

困惑

ローカルVMの奇妙さを考えると、私が最も制御できるマシンであるディレクトリをスワップしたとき、pythonとPerlを1または2として使用して実行するすべての可能なオプションでバイナリアプローチを試すつもりです。データディレクトリと一貫性を保つためにそれらを複数回実行してみますが、しばらく時間がかかり、少し頭がおかしいので、最初にブレークが必要になる場合があります。私が欲しいのは一貫性です:

---(更新4-整合性

(以下はubuntu-14.04.1-server VMで実行され、カーネルは3.13.0-35-generic#62-Ubuntuです)

データのディレクトリ1/2でPython/Perl Slurpの可能な限りあらゆる方法でテストを実行して、私は次のことを発見しました:

  • Pythonは、作成されたファイル(つまり、ddによって作成されたファイル)で常に遅い
  • Pythonは常にコピーされたファイルで高速です(つまり、cp -rで作成されます)
  • Perlは作成されたファイル(つまり、ddによって作成されたファイル)で常に高速です
  • コピーされたファイル(つまり、cp -rによって作成されたファイル)ではPerlが常に遅い

だから私はOSレベルのコピーを見て、Ubuntuでは「cp」はPythonと同じように動作するようです、つまり元のファイルでは遅く、コピーされたファイルでは速く見えます。

これが私が実行したものであり、結果は、単一のSATA HDを搭載したマシンとRAID10システムで数回実行した結果です。

$ mkdir 1
$ cd 1
$ for i in {1..50000}; do dd if=/dev/urandom of=$i.xml bs=1K count=10; done
$ cd ..
$ cp -r 1 2
$ sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ time strace -f -c -o trace.copy2c cp -r 2 2copy
    real    0m28.624s
    user    0m1.429s
    sys     0m27.558s
$ sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ time strace -f -c -o trace.copy1c cp -r 1 1copy
    real    5m21.166s
    user    0m1.348s
    sys     0m30.717s

トレース結果は時間が費やされている場所を示します

$ head trace.copy1c trace.copy2c
==> trace.copy1c <==
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 60.09    2.541250          25    100008           read
 12.22    0.516799          10     50000           write
  9.62    0.406904           4    100009           open
  5.59    0.236274           2    100013           close
  4.80    0.203114           4     50004         1 lstat
  4.71    0.199211           2    100009           fstat
  2.19    0.092662           2     50000           fadvise64
  0.72    0.030418         608        50           getdents
==> trace.copy2c <==
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 47.86    0.802376           8    100008           read
 13.55    0.227108           5     50000           write
 13.02    0.218312           2    100009           open
  7.36    0.123364           1    100013           close
  6.83    0.114589           1    100009           fstat
  6.31    0.105742           2     50004         1 lstat
  3.38    0.056634           1     50000           fadvise64
  1.62    0.027191         544        50           getdents

だからコピーのコピーは元のファイルのコピーよりもはるかに速いようですが、私の現在の推測では、コピーされたファイルは最初に作成されたときよりもディスク上で整列され、読み取りがより効率的になりますか?

興味深いことに、「rsyn」と「cp」は、PerlとPythonのように、速度的に反対の方法で機能するようです。

$ rm -rf 1copy 2copy; sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'; echo "Rsync 1"; /usr/bin/time rsync -a 1 1copy; sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'; echo "Rsync 2"; /usr/bin/time rsync -a 2 2copy
Rsync 1
    3.62user 3.76system 0:13.00elapsed 56%CPU (0avgtext+0avgdata 5072maxresident)k
    1230600inputs+1200000outputs (13major+2684minor)pagefaults 0swaps
Rsync 2
    4.87user 6.52system 5:06.24elapsed 3%CPU (0avgtext+0avgdata 5076maxresident)k
    1231832inputs+1200000outputs (13major+2689minor)pagefaults 0swaps

$ rm -rf 1copy 2copy; sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'; echo "Copy 1"; /usr/bin/time cp -r 1 1copy; sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'; echo "Copy 2"; /usr/bin/time cp -r 2 2copy
Copy 1
    0.48user 6.42system 5:05.30elapsed 2%CPU (0avgtext+0avgdata 1212maxresident)k
    1229432inputs+1200000outputs (6major+415minor)pagefaults 0swaps
Copy 2
    0.33user 4.17system 0:11.13elapsed 40%CPU (0avgtext+0avgdata 1212maxresident)k
    1230416inputs+1200000outputs (6major+414minor)pagefaults 0swaps
61
Simon

残りの部分は類似しているはずなので、私はあなたの例の1つだけに焦点を当てます。

この状況で問題になるのは、先読み(またはこれに関連する別の手法)機能です。

そのような例を考えてみましょう:

Ddコマンドで行ったように、 "1" dir(名前1.xmlから1000.xml)に1000個のxmlファイルを作成しました。その後、元のdir 1をdir 2にコピーしました。

$ mkdir 1
$ cd 1
$ for i in {1..1000}; do dd if=/dev/urandom of=$i.xml bs=1K count=10; done
$ cd ..
$ cp -r 1 2
$ sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ time strace -f -c -o trace.copy2c cp -r 2 2copy
$ sync; Sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
$ time strace -f -c -o trace.copy1c cp -r 1 1copy

次のステップでは、cpコマンドを(straceで)デバッグして、データがコピーされる順序を確認しました。

したがって、cpは次の順序でそれを行います(最初の4つのファイルのみです。元のディレクトリからの2番目の読み取りは、コピーされたディレクトリからの2番目の読み取りより時間がかかるためです)。

100.xml 150.xml 58.xml 64.xml ... *この例では

次に、これらのファイルで使用されるファイルシステムブロックを確認します(debugfs出力-ext3 fs)。

元のディレクトリ:

BLOCKS:
(0-9):63038-63047 100.xml
(0-9):64091-64100 150.xml
(0-9):57926-57935 58.xml
(0-9):60959-60968 64.xml
....


Copied directory:
BLOCKS:
(0-9):65791-65800 100.xml
(0-9):65801-65810 150.xml
(0-9):65811-65820 58.xml
(0-9):65821-65830 64.xml

....

ご覧のように、「コピーされたディレクトリ」ではブロックが隣接しているため、最初のファイル100.xmlの読み取り中に、「先読み」手法(コントローラーまたはシステム設定)がパフォーマンスを向上させることができます。

ddは1.xmlから1000.xmlの順序でファイルを作成しますが、cpコマンドはそれを別の順序(100.xml、150.xml、58.xml、64.xml)でコピーします。したがって、実行すると:

cp -r 1 1copy

このディレクトリを別のディレクトリにコピーするには、コピーしたファイルのブロックが隣接していないため、そのようなファイルの読み取りには時間がかかります。

Cpコマンドでコピーしたdirをコピーすると(ddコマンドでファイルが作成されないため)、ファイルは隣接しているため、次のように作成されます。

cp -r 2 2copy 

コピーのコピーはより高速です。

概要:python/Perlのパフォーマンスをテストするには、同じディレクトリ(またはcpコマンドでコピーした2つのディレクトリ)を使用する必要があります。また、オプションO_DIRECTを使用して、すべてのカーネルバッファーをバイパスして読み取り、ディスクから直接データを読み取ることができます。

カーネル、システム、ディスクコントローラー、システム設定、fsなどの種類によって結果が異なる可能性があることを覚えておいてください。

追加:

 [debugfs] 
[root@dhcppc3 test]# debugfs /dev/sda1 
debugfs 1.39 (29-May-2006)
debugfs:  cd test
debugfs:  stat test.xml
Inode: 24102   Type: regular    Mode:  0644   Flags: 0x0   Generation: 3385884179
User:     0   Group:     0   Size: 4
File ACL: 0    Directory ACL: 0
Links: 1   Blockcount: 2
Fragment:  Address: 0    Number: 0    Size: 0
ctime: 0x543274bf -- Mon Oct  6 06:53:51 2014
atime: 0x543274be -- Mon Oct  6 06:53:50 2014
mtime: 0x543274bf -- Mon Oct  6 06:53:51 2014
BLOCKS:
(0):29935
TOTAL: 1

debugfs:  
24
RaFD