KDEのバグトラッカーには、一部のコントリビューターと同じくらい古いバグがあります。バグ342056、「Ridiculously slow file copy (multiple small files)」は、2014年にAlexander Nestorovが報告したものです。報告は率直なものでした。約300万個の小さなファイルを含む15 GBのフォルダーをKDEでコピーするのに5〜10時間かかった一方で、rsyncでは約20分で済みました。あるコメントがその雰囲気を要約しています。
数ファイルを超える場合、私はDolphinではなくcpやrsyncを一般的に使っています。
このバグはしばらく気になっていたので、この投稿はついにそれを修正した内容です。私はKIOのコピーパスに取り組んできており、時間がどこに費やされていたのか、その理由を説明する歴史、そしてcpを参照として含むKIOバージョン間の数値を示します。
どのような最適化でも、まず測定から始めましょう。
KIO 6.28で5000個の小さなファイルをコピーする際のブロッキング時間の内訳。単一のホットスポットはありません。各ファイルは短いシステムコールのバーストでブロックします。マウントテーブル(/proc/self/mountinfo)の読み取り、ソースと宛先のオープン、そしてワーカーへの内部ソケット経由のコマンドのラウンドトリップです。ソケットのラウンドトリップ、送受信および各応答の待機は赤くハイライトされたフレームで、ここでのブロッキングの約15%を占め、すべてのファイルがそれらを負担するため、両方のスレッドに薄く広がっています。緑のフレームは、コピーが避けられない実際のファイルシステム作業です。statxおよびopenatコール、バイトを移動するcopy_file_range、その下のext4メタデータの更新です。この1ファイルごとの嵐こそが、この投稿の残りで議論するものです。(sched:sched_switchからのOff-CPUフレームグラフ、ブロッキングスイッチの数で重み付け。完全なSVGを開くにはクリックしてください。)
少し歴史
KIOは、Dolphinでのコピー&ペーストから、sftp://やsmb://のURLをローカルファイルのように開けるようにするネットワーク透過性まで、KDEソフトウェアのほぼすべてのファイル操作の背後にあるレイヤーです。その設計はKDE 2時代、2000年頃に遡ります。当時、「UIをフリーズさせずにI/Oを行うにはどうすればよいか」に対する答えはスレッドではなく、別プロセスでした。ワーカー(当時はkioslavesと呼ばれていました)はプロトコルごとに起動され、ソケット経由でアプリケーションと通信します。これはLinuxで実用的なスレッディングが可能になる前(Native POSIX Thread Libraryは2003年にLinux 2.6で登場)であり、プロセス+ソケットIPCはポータブルで堅牢な選択肢でした。現在でもネットワークプロトコルには有効です。smb://ワーカーがクラッシュしても、ファイルマネージャーが巻き込まれることはありません。
裏返しはコストです。すべてのリクエストとその応答はシリアライズされ、そのソケット経由で送信され、双方でイベントループのホップが発生します。file://の場合、そのオーバーヘッドは何も得られません。信頼できないネットワークはなく、ただローカルディスクがあるだけだからです。
2022年にDavid Faureがそれを変えました。スレッドを使用してKIOワーカーをインプロセスで実行するがFrameworks 5.95で実装されました。それ以来、fileワーカーは別プロセスではなく、アプリケーション内のスレッドで実行されるようになりました(堅牢性のため、他のプロトコルはアウトプロセスに留まります)。これにより、プロセスの起動とコンテキストスイッチのコストがなくなりました。
スレッドとアプリ間のソケットを廃止(6.29)
まだ明らかな場所に隠れていた問題がありました。インプロセスであっても、ワーカースレッドとアプリケーションはsocketpairを介して通信し、別プロセスであるかのようにすべてのコマンドをシリアライズしていました。スレッドを使用するようになった今、そのソケットは純粋なオーバーヘッドでした。
それが最初のフレームグラフの赤いハイライトです。
そこで、インプロセスワーカーはループバックソケットの代わりに実際のインメモリトランスポートを使用するようになりました。新しいThreadConnectionBackendがコマンドと、読み取り時は実際のデータバッファを、シリアライズやシステムコールなしでピアスレッドに直接処理し、インプロセスファイル読み取りをゼロコピー化します。これは下記の数値で最大の単一ジャンプであり、6.29向けのmasterに取り込まれています(kio!2279)。
次:コピーとstatのバッチ処理(レビュー中)
上記の変更により、各コマンドは安価になりました。残るコストはコマンドの数が依然として多いことです。N個のファイルをコピーするにはN個の別々のfile_copyジョブが必要で、それぞれがジョブスケジューラを通過し、ワーカーへの独自のラウンドトリップが発生します。多くの小さなファイルの場合、データではなく、この固定の1ファイルごとのコストが支配的になります。
CopyJobは、プレーンなローカル→ローカルのファイルのバッチ全体を1つのコマンドでワーカーにディスパッチでき、ワーカーはそれらをコピーします(copy_file_rangeを使用し、BtrfsやXFSのようなリフリンクをサポートするファイルシステムではreflinkを使用)。各ファイルは引き続き進捗シグナル、バイトアカウンティング、元に戻すエントリを取得し、ワーカーが盲目的に実行できないもの(既存の宛先、読み取れないソース、名前変更やスキップの決定)は通常の1ファイルごとのパスに戻されます。ゲートは意図的に保守的で、ハングしたマウントがバッチをフリーズさせることはありません(kio!2282)。
バッチコピー後の6.29での同じコピー。赤いソケットのラウンドトリップはなくなりました。それらを除去したのは2つのことです。前セクションのインメモリトランスポート(socketpair自体がない)と、バッチコピーによる複数のファイル全体を1コマンドにまとめることです。残るのは実際のファイルシステム作業です。緑のフレームはコピー自体、バイトを移動するcopy_file_range、openatおよびstatxコール、そしてその下のext4メタデータです。緑でない広い台地は、次の実行前に各パスのファイルを削除するベンチマークループ(unlink)で、コピーの一部ではなくティアダウンです。
コピー前に、CopyJobはすべてのソースをstatします。これは別の1ファイルごとのラウンドトリップです。フォローアップでは、同じインプロセスレーンでstatフェーズをバッチ処理します。これが下記のbatch-copy-stat列です。
どちらもレビュー中で6.29以降のリリースをターゲットにしているため、テーブル内の列は今日インストールできるものではなく、将来のプレビューとして扱ってください。
数値
方法論:13世代Intel Core i7-1365Uラップトップ、すべてをサニタイザーなしのRelWithDebInfoでビルドし、実在のext4ボリュームにコピー(リフリンクのショートカットなし、copy_file_rangeが実際のバイトを移動)。各KIOバージョンは独自のlibKIOCoreとkio_fileワーカーを提供します。時間は5回の実行(大規模セットは3回)の中央値で、ウォームアップは破棄済みなので、キャッシュはすべてのバージョンで温かく等しくなっています。cp -r --preserve=mode,timestampsをフロアとして含めています。これはバグ報告者がDolphinの代わりに手にしたものです。
4 KBのファイルが5000個(バグの多数の小さなファイルケース):
| バージョン | 中央値時間 |
|---|---|
cp -r (coreutils) | 81 ms |
| KIO 6.28.0 | 1616 ms |
| KIO 6.29-dev (master) | 396 ms |
| KIO 6.29-dev + batch-copy | 88 ms |
| KIO 6.29-dev + batch-copy-stat | 93 ms |
5 MBのファイルが1000個(コピーがI/Oバウンドになるバルクデータ):
| バージョン | 中央値時間 |
|---|---|
cp -r (coreutils) | 1312 ms |
| KIO 6.28.0 | 1997 ms |
| KIO 6.29-dev (master) | 1687 ms |
| KIO 6.29-dev + batch-copy | 1506 ms |
| KIO 6.29-dev + batch-copy-stat | 1454 ms |
小さなファイルのケースが本題です。KIO 6.28はcpより約20倍遅く、これは2014年の報告が不満を述べていた比率そのものです。インプロセスワーカーのソケットを除去し、宛先ファイルシステムへのプローブを1ファイルごとに行わなくなったことで、約1.6秒から0.4秒(約4倍)に短縮されました。コピーをバッチ処理することで88ミリ秒になり、事実上cpと同等の速度で、6.28より約18倍高速になりました。あの「Dolphinの代わりにcpを使う」という文言に、ついに答えが出ました。
大容量ファイルのケースは、この作業以前からすでに十分にカバーされており、表が示しています。コピーはバイトの移動に縛られるため、KIO 6.28でもcpに近い値に留まり、1ファイルごとの最適化は数値をほとんど動かしません(batch-copyはcpの約15%以内に収まります)。この取り組みが、データではなく固定の1ファイルごとのコストが支配的な多数の小さなファイルをターゲットにする理由がそこにあります。
batch-copyとbatch-copy-statはこのテストでは互角でした。ベンチマークは単一ディレクトリをコピーするため、そのエントリは1回のリスト取得で到着し、batch-statがバッチ処理するものはありません。個別のファイルの大きな選択範囲をコピーするもう一方のケースでは役立ちます。そうでなければ1ファイルごとにstatのラウンドトリップが発生するからです。ラウンドトリップを除去するものでありstatコールを除去するものではないため、ディスクが遅い場合ではなく、statが安価な場合に最も効果を発揮します。このテストでは2つを同等と読んでください。
そしてcpは設計上、わずかに先行し続けるでしょう。KIOはバイトのコピー以上のことを行います。1ファイルごとの進捗を報告し、一時停止やキャンセルが可能で、元に戻す記録を保持し、パーミッションとタイムスタンプを保持し、競合を解決し、変更をオープンなファイルマネージャーに通知してビューを更新します。この作業は無料ではなく、目標はcpを上回ることではなく、ローカルケースを十分に高速にして、Dolphinの代わりにcpを選ぶことが当然の選択でなくなるようにすることでした。
注意事項と今後の予定
これらは、1台のext4ディスク上でウォームキャッシュのラップトップでの数値であり、絶対値ではなくバージョンを比較するためのものです。BtrfsやXFSではバッチパスがコピーではなくreflinkを行うため、大容量ファイルの状況は根本的に変わります。バッチ高速パスは、応答性の高いファイルシステム上のプレーンなローカル→ローカルコピーの場合にのみ有効です。それ以外(ネットワーク、FAT/NTFS、ダイアログを必要とする競合)は、既存の1ファイルごとのパスにフォールバックし、変更はありません。
このすべてを可能にしたインプロセスワーカースレッドを提供してくれたDavid Faure、以前のリスト取得の修正を行ってくれたFrank Reininghaus、そして十分に古くなって今も修正する価値のあるバグ報告をしてくれたAlexander Nestorovに感謝します。
以上です。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.