paste
は素晴らしいツールですが、実行時にサーバー上で約50MB / sで非常に遅いです。
paste -d, file1 file2 ... file10000 | pv >/dev/null
paste
によると、100%CPUを使用するため、top
ディスク速度の低下などの要因によって制限されません。
ソースコードを見ると、次のものが使用されている可能性がありますgetc
。
while (chr != EOF)
{
sometodo = true;
if (chr == line_delim)
break;
xputchar (chr);
chr = getc (fileptr[i]);
err = errno;
}
同じことをしますが、より速く実行できる他のツールはありますか?一度に4k-64kチャンクを読むことができますか?一度に1バイトずつ表示する代わりに、ベクトル命令を使用して並行して改行文字を見つけることは可能ですか?おそらく使用するかawk
似ていますか?
入力ファイルはUTF8で、大きすぎてRAMに収まらないので読み込みすべてメモリに入るのはオプションではありません。
編集する:
Thanasispでは、ジョブを並列に実行することをお勧めします。これはスループットをわずかに向上させますが、まだ純粋なものよりもはるかに遅くなりますpv
。
# Baseline
$ pv file* | head -c 10G >/dev/null
10.0GiB 0:00:11 [ 897MiB/s] [> ] 3%
# Paste all files at once
$ paste -d, file* | pv | head -c 1G >/dev/null
1.00GiB 0:00:21 [48.5MiB/s] [ <=> ]
# Paste 11% at a time in parallel, and finally paste these
$ paste -d, <(paste -d, file1*) <(paste -d, file2*) <(paste -d, file3*) \
<(paste -d, file4*) <(paste -d, file5*) <(paste -d, file6*) \
<(paste -d, file7*) <(paste -d, file8*) <(paste -d, file9*) |
pv | head -c 1G > /dev/null
1.00GiB 0:00:14 [69.2MiB/s] [ <=> ]
top
それでも、外部ソースがpaste
ボトルネックを引き起こしているようです。
バッファを増やすと違いがあるかどうかをテストしました。
$ stdbuf -i8191 -o8191 paste -d, <(paste -d, file1?) <(paste -d, file2?) <(paste -d, file3?) <(paste -d, file4?) <(paste -d, file5?) <(paste -d, file6?) <(paste -d, file7?) <(paste -d, file8?) <(paste -d, file9?) | pv | head -c 1G > /dev/null
1.00GiB 0:00:12 [80.8MiB/s] [ <=> ]
これによりスループットが 10% 増加します。バッファを増やしても何も改善されませんでした。これはハードウェアに関連している可能性があります(つまり、レベル1のCPUキャッシュのサイズが原因である可能性があります)。
テストは、ディスクサブシステムに関連する制限を避けるためにRAMディスクで実行されます。
答え1
さまざまな選択肢とシナリオを使用した追加テスト
(編集:コンパイルされたバージョンのカットオフ値を追加)
簡単に言うと:
- はい、coreutils
paste
が良いですcat
paste
特に、大量の短い行では、より高速なcoreutilsの代わりに簡単に使用できる代替手段はないようです。paste
スループットは、ライン長、ライン数、ファイル数のさまざまな組み合わせで驚くほど安定しています。- より長い路線の場合、より速い代替案が以下に提供される。
詳細:
多くのシナリオをテストしました。スループット測定はpv
もともと投稿と同様に行われた。
比較オプション:
cat
(基準としてGNU coreutils 8.25から)paste
(また、GNU coreutils 8.25で)- Pythonスクリプト~から上記で回答しました
- 代替Pythonスクリプト(行フラグメントを収集するためのリストの理解を通常のループに置き換えます)
- プロジェクト(4に似ていますが、実行可能ファイルとしてコンパイルされています)
ファイル/行番号の組み合わせ:
# | リスト | ワイヤー |
---|---|---|
1 | 200,000 | 1,000 |
2 | 20,000 | 10,000 |
サム | 2,000 | 100,000 |
4 | 200 | 1,000,000 |
5 | 20 | 10,000,000 |
6 | 2 | 100,000,000 |
各テストの総データ容量は1.3GBと同じであった。各列は6桁の数字(000'001〜200'000など)で構成されています。上記の組み合わせは、1、10、100、1'000、10'000の同じサイズのファイルにできるだけ多く配布されます。
生成されたファイルは次のとおりです。yes {000001..200000} | head -1000 > 1
貼り付けは次のように行われます。for i in cat paste ./paste ./paste2 ./paste3; do $i {00001..1000} | pv > /dev/null; done
ただし、貼り付けたファイルは実際にはすべて同じソースファイルへのリンクなので、すべてのデータはとにかくキャッシュに存在する必要があります(貼り付けの直前に作成され、最初に読み込まれます;cat
システムメモリは128 GB、キャッシュサイズ34 GB)
あらかじめ作成されたファイルから読み込んでパイプに接続する代わりに、データをすぐに生成する別のセットが実行されましたpaste
(下の表示、ファイル数= 0)。
最後のコマンドセットの場合、次のようになります。for i in cat paste ./paste ./paste2 ./paste3; do $i <(yes {000001..200000} | head -1000) | pv > /dev/null; done
発見する:
paste
数十倍遅い速度cat
paste
スループットは、さまざまな線幅と関連ファイルの数にわたって非常に一定です(約300 MB / s)。- 平均入力ファイル行の長さが特定の制限(私のテストコンピュータでは約1400文字)を超えると、自家製のPythonの代替案がいくつかの利点を示すことがあります。
- Pythonスクリプトと比較してコンパイルされたnimバージョンは、スループットが約2倍です。これに対して、
paste
入力ファイルの損益分岐点は約500文字です。この値は、入力ファイルの数が増えるにつれて減少し、入力ファイルが10個以上含まれると、入力ファイルの行ごとに約150文字まで減ります。 - Pythonとnimの両方のバージョンは、多くの短い行の処理オーバーヘッドによって困難に直面しています(疑わしい原因:使用されている2つのstdlib関数で行の終わりを検出し、それをプラットフォーム固有の端に変換しようとしました)。ただし、coreutilsは
paste
影響を受けません。 cat
同時動的データ生成プロセスは、行が長いnimバージョンの制限要因であり、処理速度にある程度影響を与えるようです。- ある時点では、多数のオープンファイルハンドルがcoreutilsに悪影響を及ぼすように見えます
paste
。 (ただ推測です。たぶんこれは並列バージョンにも影響しますか?)
結論(少なくともテストマシンの場合)
- 入力ファイルが狭い場合、特にファイルが長い場合はcoreutils Pasteを使用してください。
- 入力ファイルがかなり広い場合は、選択肢が優先されます(入力ファイルの数に応じて、Pythonバージョンの場合は入力ファイルの行の長さ> 1400文字、nimバージョンの場合は150〜500文字)。
- 通常、Pythonスクリプトの代わりにコンパイルされたnimバージョンを好みます。
- 小さな部分が多すぎる場合は注意してください。この場合、プロセスのデフォルトのソフト制限である1024のオープンファイルはかなり合理的に見えます。
OP状況に対する提案(並列処理)
入力ファイルが狭い場合は、paste
内部操作でcoreutilsを使用し、コンパイルされた代替最も外側のプロセスの場合。すべてのファイルに長い行がある場合は、通常はnimバージョンを使用してください。
洞窟:リンクされたプログラムは一時的なリリースであり、いかなる保証や明示的なエラー処理もせずに「現状のまま」提供されます。さらに、3つの実装すべてで区切り文字がハードコードされています。
答え2
getcは高度に最適化されており、現在主にバッファ対応マクロとして実装されていますが、まだボトルネックが発生する可能性があることに同意します。代わりに、使用されるバッファのサイズが比較的小さいことがあるので、依然として高い結果を生むことができる。ファイルの読み取り回数。
上記の正確な数値は得られませんでしたが、テストでは、ベースラインランとペーストランの間にはまだ顕著な違いがありました。
(たぶん、犯人はバッファリング(ファイルブロック、ストリームライン)のスイッチかもしれませんが、私はそのような経験がありません。)
さらなる試験により、以下の構造を使用する場合、スループットが同様に減少することが確認された。
cat file* | dd | pv > /dev/null
中間インサートddのスループットはペーストの実行のスループットと非常に類似している。 (ddはデフォルトで512バイトのブロックサイズを使用します)。バッファサイズをさらに小さくすると、それに比例して実行時間も長くなります。ただし、ブロックサイズをわずか数kb(8または16など)に増やすと、速度が大幅に増加します。 1または2Mを使用すると離陸しました。
cat file* | dd bs=2M | pv > /dev/null
getcのバッファサイズを変更することができるようです(stdoutがラインバッファリングから自由に選択されたサイズのブロックバッファリングに切り替えられる場合)。ただし、開いている数千のファイルがあり、各ファイルにバッファがある場合は、メモリ要件が急速に増加する可能性があることに注意してください。
それにもかかわらず、バッファリングを変更してみることができます(例:https://stackoverflow.com/questions/66219179/usage-of-getc-with-a-file)適切なsetvbuf呼び出しを使用して何が起こるかを確認してください。
次へ追加: 現時点では、同じタスクをより速く実行する同様の公開プログラムを知りません。
(PS:名前を見たばかりです。GNUパラレルの専門家ですか?お疲れ様でした!)
答え3
私は過去に非常に高速なPythonテキスト処理(つまり、高度に最適化され、高度に調整されたPythonテキスト処理ルーチン)を経験したので、小さなq&d Pythonスクリプトを一緒にスローしてcoreutilsの貼り付けと比較しました。 (もちろん、このオプションはデモ用にのみ使用され(現在のファイルのファイル名のみを許可)、列区切り文字が組み込まれているため、オプションは非常に制限されています。)
現在のディレクトリにあるテキストファイルに入れてpaste
実行権限を与えてみてください。
#! /usr/bin/env python3
import sys
filenames = sys.argv[1:]
infiles = [open(i,'r') for i in filenames]
while True:
lines = [i.readline() for i in infiles]
if all([i=='' for i in lines]):
break
print("\t".join([i.strip("\n") for i in lines]))
修正する:すべての入力ファイルで同じ行が空であることが検出された場合、スクリプトがすぐに中断されるようにする上記のスクリプトのバグを修正しました。 Pythonバージョンのタイミング測定も、実際の実行時間を反映するように以下に更新されました。
複数のシステムでキャッシュをウォームアップした後(RAMディスクを作成する必要なしに)テストしました。上記のPythonバージョンは常にcoreutils Pasteよりも優れています。範囲は10%(デフォルトのDebian Buster)から30%(Debian Stretchを実行しているVM)です。 Windowsでは違いがより顕著ですが、これは追加のposix変換オーバーヘッドまたは他のキャッシュによって引き起こされる可能性があります(coreutils Paste 8.26を使用するcygwin:> 20x遅い; coreutils Paste 8.32を使用するmsys2:Pythonのバージョン時間より> 12x遅い; どちらもPythonを実行します)。 3.9.9)このテストでは長い行を処理するのに問題があるようだったので、非常に長い100行のファイルを作成してファイル自体に貼り付けました。
jf1@s1 MSYS /d/temp/ui
# time paste b b > /dev/null
real 0m5.920s
user 0m5.896s
sys 0m0.031s
jf1@s1 MSYS /d/temp/ui
# time ../paste b b > /dev/null
real 0m0.480s
user 0m0.295s
sys 0m0.170s
jf1@s1 MSYS /d/temp/ui
# ../paste b b > c1
jf1@s1 MSYS /d/temp/ui
# paste b b > c2
jf1@s1 MSYS /d/temp/ui
# diff c1 c2
jf1@s1 MSYS /d/temp/ui
#