質問

サイズが 100GB 以上になる可能性のあるファイルを処理するプログラムに取り組んでいます。ファイルには可変長レコードのセットが含まれています。最初の実装を立ち上げて実行しており、現在はパフォーマンスの向上、特に入力ファイルが何度もスキャンされるため I/O をより効率的に実行することを目指しています。

使用に関する経験則はありますか mmap() C++ 経由でのブロックの読み取りと比較 fstream 図書館?私がやりたいのは、大きなブロックをディスクからバッファに読み取り、バッファから完全なレコードを処理して、さらに読み取ることです。

mmap() コードが非常に乱雑になる可能性があるため、 mmap'd ブロックはページ サイズの境界上にある必要があり (私の理解では)、レコードはページ境界を越えて配置される可能性があります。と fstreamページ サイズの境界にあるブロックの読み取りに限定されないため、レコードの先頭までシークして再度読み取りを開始できます。

最初に完全な実装を実際に作成せずに、これら 2 つのオプションのどちらかを決定するにはどうすればよいでしょうか?経験則 (例: mmap() は 2 倍高速です) または単純なテストですか?

役に立ちましたか?

解決

Linux での mmap / 読み取りパフォーマンスに関する最後の言葉を見つけようとしていたところ、素晴らしい投稿を見つけました (リンク) Linux カーネル メーリング リストにあります。2000 年のことなので、それ以来、カーネルの IO と仮想メモリには多くの改良が加えられてきましたが、その理由をうまく説明しています。 mmap または read 速いかもしれないし、遅いかもしれない。

  • への電話 mmap よりもオーバーヘッドが大きい read (と同じように epoll よりもオーバーヘッドが大きい poll, 、より多くのオーバーヘッドがあります read)。仮想メモリ マッピングの変更は、異なるプロセス間の切り替えにコストがかかるのと同じ理由から、一部のプロセッサでは非常にコストのかかる操作です。
  • IO システムはすでにディスク キャッシュを使用できるため、ファイルを読み取る場合、どの方法を使用しても、キャッシュにヒットするかミスする可能性があります。

しかし、

  • 一般に、メモリ マップは、特にアクセス パターンがまばらで予測不可能な場合、ランダム アクセスの方が高速です。
  • メモリマップにより、次のことが可能になります。 保つ 完了するまでキャッシュのページを使用します。つまり、ファイルを長期間頻繁に使用した後、ファイルを閉じて再度開いた場合でも、ページはキャッシュされたままになります。と read, 、ファイルはかなり前にキャッシュからフラッシュされている可能性があります。ファイルを使用してすぐに破棄した場合は、この限りではありません。(そうしようとすると mlock ページをキャッシュに保持するためだけにページを作成すると、ディスク キャッシュを出し抜こうとすることになり、この種の愚かな行為がシステム パフォーマンスに役立つことはほとんどありません)。
  • ファイルを直接読み取るのは非常に簡単かつ高速です。

mmap/read についての議論は、他の 2 つのパフォーマンスに関する議論を思い出させます。

  • 一部の Java プログラマは、ノンブロッキング I/O がブロッキング I/O よりも遅いことが多いことを発見してショックを受けました。ノンブロッキング I/O ではより多くの syscall が必要であることを知っていれば、これは当然のことです。

  • 他のネットワーク プログラマの中には、このことを知ってショックを受けた人もいます。 epoll 多くの場合、それよりも遅いです poll, を管理することを知っていれば、これは完全に理にかなっています。 epoll より多くのシステムコールを行う必要があります。

結論: データにランダムにアクセスする場合、データを長期間保持する場合、またはデータを他のプロセスと共有できることがわかっている場合は、メモリ マップを使用します(MAP_SHARED 実際の共有がなければあまり面白くありません)。データに順次アクセスする場合は通常どおりファイルを読み取るか、読み取り後に破棄します。どちらかの方法でプログラムの複雑さが軽減される場合は、次のようにします。 それ. 。実際の多くのケースでは、ベンチマークではなく実際のアプリケーションをテストすることなしに、どちらかが高速であることを確実に示す方法はありません。

(この質問を壊して申し訳ありませんが、答えを探していたところ、この質問が Google の検索結果の一番上に表示され続けました。)

他のヒント

主なパフォーマンスコストはディスク I/O になります。「mmap()」は確かに istream よりも高速ですが、ディスク I/O が実行時間の大半を占めるため、違いは目立たないかもしれません。

Ben Collins のコード断片 (上記/下記を参照) を試して、「mmap() は 方法 より速くなりました」と測定可能な差は見つかりませんでした。彼の答えに対する私のコメントを参照してください。

確かにそうするだろう ない 「レコード」が巨大でない限り、各レコードを順番に個別に mmap することをお勧めします。これでは恐ろしく時間がかかり、レコードごとに 2 つのシステム コールが必要になり、ディスク メモリ キャッシュからページが失われる可能性があります。

あなたの場合、mmap()、istream、および低レベルの open()/read() 呼び出しはすべてほぼ同じになると思います。次のような場合には mmap() をお勧めします。

  1. ファイル内にランダム アクセス (シーケンシャルではない) があり、かつ
  2. 全体がメモリに快適に収まるか、ファイル内に参照の局所性があり、特定のページをマップインし、他のページをマップアウトできます。そうすることで、オペレーティング システムは利用可能な RAM を最大限に活用します。
  3. または、複数のプロセスが同じファイルを読み取り/作業している場合、すべてのプロセスが同じ物理ページを共有するため、 mmap() は優れています。

(ところで、私は mmap()/MapViewOfFile() が大好きです)。

mmap は 方法 もっと早く。それを自分で証明するために、簡単なベンチマークを作成することもできます。

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

対:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

明らかに、詳細は省略しています(ファイルが次の倍数ではない場合に、ファイルの終わりにいつ到達するかを判断する方法など) page_size, 、たとえば)、実際にはこれよりもはるかに複雑になるはずはありません。

可能であれば、データを複数のファイルに分割して、部分的ではなく全体的に mmap() できるようにすることをお勧めします (はるかに簡単です)。

数か月前、私は boost_iostreams 用のスライディング ウィンドウ mmap() によるストリーム クラスの中途半端な実装を作成しましたが、誰も気に留めず、私は他のことで忙しくなってしまいました。最も残念なことに、私は数週間前に古い未完成プロジェクトのアーカイブを削除しましたが、それも被害者の 1 つでした :-(

アップデート:また、Microsoft は最初に mmap で行うことのほとんどを行う気の利いたファイル キャッシュを実装しているため、このベンチマークは Windows ではまったく異なるものになるという点にも注意を付け加えておきます。つまり、頻繁にアクセスされるファイルの場合、単に std::ifstream.read() を実行すると、ファイル キャッシュがすでにメモリ マッピングを行っており、透過的であるため、mmap と同じくらい高速になります。

最終更新:見てください、皆さん:OS、標準ライブラリ、ディスク、メモリ階層のさまざまなプラットフォームの組み合わせでは、システム コールがどのように機能するかについて確信を持って言うことはできません。 mmap, は、ブラック ボックスと見なされ、常に常に常に、より大幅に高速になります。 read. 。私の言葉がそのように解釈されたとしても、それはまさに私の意図ではありませんでした。 最終的に私が言いたいことは、メモリマップド I/O は一般にバイトベース I/O よりも高速であるということでした。これは今でも真実です. 。実験的にこの 2 つに違いがないことがわかった場合、私にとって合理的と思われる唯一の説明は、プラットフォームが、呼び出しのパフォーマンスに有利な方法で内部でメモリ マッピングを実装しているということです。 read. 。メモリマップド I/O をポータブルな方法で使用していることを確実に確認する唯一の方法は、次のようにすることです。 mmap. 。移植性を気にせず、ターゲット プラットフォームの特定の特性に依存できる場合は、次を使用します。 read パフォーマンスを大幅に犠牲にすることなく適している可能性があります。

編集して回答リストを整理します。@jbl:

スライディングウィンドウMMAPは面白そうです。もう少し言えますか?

確かに、私は Git 用の C++ ライブラリ (libgit++) を作成していましたが、これと同様の問題に遭遇しました。大きな (非常に大きな) ファイルを開くことができ、パフォーマンスが完全に低下しないようにする必要がありました。 std::fstream).

Boost::Iostreams すでにmapped_fileソースがありますが、問題はそれが mmapファイル全体に ping を実行すると、2^(ワードサイズ) に制限されます。32 ビット マシンでは、4GB では十分な容量ではありません。期待するのも無理はない .pack Git 内のファイルはそれよりもはるかに大きくなるため、通常のファイル I/O に頼らずにファイルを分割して読み取る必要がありました。のカバーの下で Boost::Iostreams, 、ソースを実装しました。これは、多かれ少なかれ、間の相互作用の別のビューです。 std::streambuf そして std::istream. 。継承するだけで同様のアプローチを試すこともできます std::filebufmapped_filebuf そして同様に、継承します std::fstream の中へ a mapped_fstream. 。両者の相互作用を正しく理解するのは難しい。 Boost::Iostreams にはいくつかの作業が行われ、フィルターとチェーンのフックも提供されるため、そのように実装する方が便利だと思いました。

ここには、重要な点の多くをカバーする優れた回答がすでにたくさんあるので、上で直接取り上げられなかった問題をいくつか追加するだけです。つまり、この回答は長所と短所の包括的なものではなく、ここでの他の回答への補足とみなされるべきです。

mmap は魔法のようです

ファイルがすでに完全にキャッシュされている場合を考える1 ベースラインとして2, mmap かなり似ているかもしれない 魔法:

  1. mmap ファイル全体を (潜在的に) マッピングするために必要なシステム コールは 1 つだけで、その後はシステム コールは必要ありません。
  2. mmap カーネルからユーザー空間へのファイル データのコピーは必要ありません。
  3. mmap ファイルに「メモリとして」アクセスできます。これには、コンパイラの自動ベクトル化など、メモリに対して実行できる高度なトリックを使用してファイルを処理することも含まれます。 SIMD 組み込み、プリフェッチ、最適化されたメモリ内解析ルーチン、OpenMP など。

ファイルがすでにキャッシュにある場合、次のことを克服するのは不可能と思われます。カーネル ページ キャッシュにメモリとして直接アクセスするだけで、それより高速になることはありません。

まあ、それはできます。

mmap は実際には魔法ではありません。なぜなら...

mmap は引き続きページごとの作業を行います

主な隠れコストは、 mmapread(2) (これは実際には、同等の OS レベルのシステムコールです。 ブロックの読み取り)それは mmap ページフォールトメカニズムによって隠されている可能性がある場合でも、ユーザースペース内のすべての 4K ページに対して「何らかの作業」を行う必要があります。

たとえば、典型的な実装は次のとおりです。 mmap■ ファイル全体をフォールトインする必要があるため、100 GB のファイルを読み取るには 100 GB / 4K = 2,500 万回のフォールトが発生します。さて、これらは次のようになります 軽微な欠陥, しかし、250 億のページフォールトはまだ超高速にはなりません。軽微な障害のコストは、おそらく最良の場合でも数百ナノ単位です。

mmap は TLB パフォーマンスに大きく依存します

これで、合格できます MAP_POPULATEmmap これは、アクセス中にページ フォールトが発生しないように、戻る前にすべてのページ テーブルをセットアップするように指示するためです。さて、これにはファイル全体を RAM に読み込むという小さな問題があり、100 GB のファイルをマップしようとすると爆発してしまいますが、今はそれを無視しましょう3. 。カーネルが行う必要があるのは、 ページごとの作業 これらのページ テーブルをセットアップします (カーネル時間として表示されます)。これは最終的に大きなコストになります。 mmap これはファイル サイズに比例します (つまり、ファイル サイズが大きくなっても重要性が相対的に低下することはありません)。4.

最後に、ユーザー空間であっても、このようなマッピングへのアクセスは完全に無料というわけではありません (ファイルベースではない大規模なメモリ バッファと比較して) mmap) - ページ テーブルが設定された後でも、概念的には、新しいページにアクセスするたびに TLB ミスが発生します。以来 mmapファイルを作成するということは、ページ キャッシュとその 4K ページを使用することを意味し、100 GB のファイルで 2,500 万倍のコストが再び発生します。

これらの TLB ミスの実際のコストは、ハードウェアの少なくとも次の側面に大きく依存します。(a) 4K TLB エンティティの数と、残りの変換キャッシュ動作がどのように実行されるか (b) ハードウェア プリフェッチが TLB をどの程度適切に処理するか - たとえば、プリフェッチはページ ウォークをトリガーできますか?(c) ページ ウォーキング ハードウェアの速度と並列度。最新のハイエンド x86 Intel プロセッサでは、ページ ウォーキング ハードウェアは一般に非常に強力です。少なくとも 2 つの並列ページ ウォーカーがあり、継続的な実行と同時にページ ウォークが発生する可能性があり、ハードウェア プリフェッチがページ ウォークをトリガーする可能性があります。したがって、TLB の影響は ストリーミング 読み取り負荷はかなり低く、そのような負荷は多くの場合、ページ サイズに関係なく同様に実行されます。ただし、他のハードウェアは通常、はるかに悪いです。

read() はこれらの落とし穴を回避します

read() syscall は、C、C++、その他の言語で提供される「ブロック読み取り」タイプの呼び出しの基礎となるものですが、誰もがよく知っている主な欠点が 1 つあります。

  • read() N バイトの呼び出しでは、N バイトをカーネルからユーザー空間にコピーする必要があります。

一方、上記のコストのほとんどが回避されます。2,500 万の 4K ページをユーザー空間にマッピングする必要がありません。通常はできます malloc ユーザー空間に単一の小さなバッファーを用意し、それをすべてのユーザーに対して繰り返し再利用します。 read 呼び出します。カーネル側では、すべての RAM が通常、いくつかの非常に大きなページ (x86 の 1 GB ページなど) を使用して線形にマップされ、ページ キャッシュ内の基礎となるページがカバーされるため、4K ページや TLB ミスの問題はほとんどありません。カーネル空間で非常に効率的に実行されます。

したがって、基本的には、大きなファイルの 1 回の読み取りでどちらが速いかを判断するには、次の比較を行う必要があります。

ページごとの追加作業は、 mmap このアプローチは、ファイルの内容をカーネルからユーザー空間にコピーするバイト単位の作業よりもコストがかかります。 read()?

多くのシステムでは、実際にはほぼバランスが取れています。それぞれがハードウェアと OS スタックのまったく異なる属性に応じて拡張されることに注意してください。

特に、 mmap 次の場合、アプローチは比較的速くなります。

  • OS は、軽微な障害を高速に処理し、特にフォールト アラウンドなどの軽微な障害の一括最適化を備えています。
  • OSには優れた機能があります MAP_POPULATE たとえば、基礎となるページが物理メモリ内で連続している場合に、大きなマップを効率的に処理できる実装です。
  • このハードウェアは、大規模な TLB、高速な第 2 レベル TLB、高速かつ並列のページ ウォーカー、翻訳との良好なプリフェッチ相互作用など、強力なページ変換パフォーマンスを備えています。

...一方 read() 次の場合、アプローチは比較的速くなります。

  • read() syscall はコピー パフォーマンスが優れています。例:良い copy_to_user カーネル側のパフォーマンス。
  • カーネルには、メモリをマッピングするための効率的な (ユーザーランドと比較した) 方法があります。たとえば、ハードウェア サポートのある少数の大きなページのみを使用します。
  • カーネルには高速 syscall と、syscall 間でカーネル TLB エントリを維持する方法があります。

上記のハードウェア要因は異なります 乱暴に 異なるプラットフォーム間、さらには同じファミリー内 (例: x86 世代内、特に市場セグメント内)、さらにはアーキテクチャ間 (例: ARM vs x86 vs PPC) です。

OS 要因も同様に変化し続けており、両側のさまざまな改善により、いずれかのアプローチで相対速度が大幅に上昇します。最近のリストには次のものが含まれます。

  • 上で説明したフォールトアラウンド機能の追加。これは非常に役立ちます。 mmap ケースなし MAP_POPULATE.
  • ファストパスの追加 copy_to_user のメソッド arch/x86/lib/copy_user_64.S, 、例えば、を使用して REP MOVQ 速いときは本当に助かります read() 場合。

SpectreとMeltdown後のアップデート

Spectre および Meltdown の脆弱性に対する緩和策により、システム コールのコストが大幅に増加しました。私が測定したシステムでは、「何もしない」システム コールのコスト (コールによって実行される実際の作業とは別に、システム コールの純粋なオーバーヘッドの推定値) は、通常のシステム コールでは約 100 ns でした。最新の Linux システムでは約 700 ns に達します。さらに、システムによっては、 ページテーブルの分離 Meltdown に特化した修正は、TLB エントリをリロードする必要があるため、直接的なシステム コール コストとは別に、追加のダウンストリーム効果をもたらす可能性があります。

これらすべては、にとって相対的な不利な点です。 read() ベースのメソッドと比較して mmap ベースのメソッドなので、 read() メソッドは、「バッファ サイズ」相当のデータごとに 1 つのシステム コールを実行する必要があります。通常、大きなバッファを使用すると L1 サイズを超えてパフォーマンスが低下し、常にキャッシュ ミスが発生するため、このコストを返済するためにバッファ サイズを任意に増やすことはできません。

一方、 mmap, を使用すると、メモリの大きな領域にマッピングできます。 MAP_POPULATE 1 回のシステムコールのみで効率的にアクセスできます。


1 これには、多かれ少なかれ、ファイルが最初から完全にキャッシュされていないが、OS の先読み機能が十分に機能しているため、そのように見える場合も含まれます (つまり、ページは通常、必要な時点までにキャッシュされています)それ)。ただし、先読みの動作方法は、 mmap そして read で説明されているように、「アドバイス」呼び出しによってさらに調整できます。 2.

2 ...なぜなら、ファイルが ない キャッシュされている場合、あなたの動作は、基盤となるハードウェアに対するアクセス パターンがどの程度共感的であるかなど、IO に関する懸念によって完全に支配されることになります。そして、そのようなアクセスが可能な限り共感的であることを保証することに全力を注ぐ必要があります。を使用して madvise または fadvise 呼び出し (およびアクセス パターンを改善するために実行できるアプリケーション レベルの変更)。

3 たとえば、次のようにしてこれを回避できます。 mmap小さいサイズ (たとえば 100 MB) のウィンドウで実行します。

4 実際、判明したのは、 MAP_POPULATE このアプローチ (少なくとも 1 つのハードウェアと OS の組み合わせ) は、それを使用しない場合よりもわずかに速いだけです。これは、おそらくカーネルが使用しているためです。 フォールトアラウンド - したがって、軽微な障害の実際の数は 16 分の 1 程度に減少します。

Ben Collins がスライディング ウィンドウの mmap ソース コードを紛失してしまったことを残念に思います。ブーストにあればいいですね。

はい、ファイルのマッピングははるかに高速です。基本的に、OS 仮想メモリ サブシステムを使用して、メモリとディスクを関連付けたり、その逆を行ったりすることになります。次のように考えてみましょう。OS カーネル開発者がそれを高速化できるのであれば、そうするでしょう。そうすることで、ほぼすべてのことが高速になるためです。データベース、起動時間、プログラムのロード時間など。

スライディング ウィンドウのアプローチは、複数の連続したページを一度にマッピングできるため、実際にはそれほど難しくありません。したがって、単一レコードの最大値がメモリに収まる限り、レコードのサイズは重要ではありません。大切なのは帳簿の管理です。

レコードが getpagesize() 境界で始まらない場合、マッピングは前のページから開始する必要があります。マップされる領域の長さは、レコードの最初のバイト (必要に応じて getpagesize() の最も近い倍数に切り捨てられる) からレコードの最後のバイト (getpagesize() の最も近い倍数に切り上げられる) までです。レコードの処理が終了したら、unmap() して次のレコードに進むことができます。

これは Windows でも、CreateFileMapping() と MapViewOfFile() (および SYSTEM_INFO.dwPageSize ではなく SYSTEM_INFO.dwAllocationGranularity を取得するための GetSystemInfo()) を使用して、すべて問題なく動作します。

mmap の方が速いはずですが、どれくらい速いかはわかりません。それはコードに大きく依存します。mmap を使用する場合は、ファイル全体を一度に mmap することをお勧めします。これにより、作業が大幅に楽になります。潜在的な問題の 1 つは、ファイルが 4GB より大きい場合 (実際には制限はさらに低く、多くの場合 2GB)、64 ビット アーキテクチャが必要になることです。したがって、32 環境を使用している場合は、おそらくそれを使用したくないでしょう。

そうは言っても、パフォーマンスを向上させるためのより良い方法があるかもしれません。あなたが言った 入力ファイルが何度もスキャンされる, 1 回のパスで読み取って完了することができれば、はるかに高速になる可能性があります。

mmap によるファイル I/O が高速になるということには同意しますが、コードのベンチマークを行う際に、反例として次のようなことを行うべきではないでしょうか。 幾分 最適化されましたか?

ベン・コリンズは次のように書いています。

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

次のことも試してみることをお勧めします。

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

さらに、0x1000 がマシン上の仮想メモリの 1 ページのサイズではない場合に備えて、バッファ サイズを仮想メモリの 1 ページと同じサイズにしてみることもできます。私の意見では、mmap で作成されたファイル I/O が依然として優先されますが、これにより状況はより近くなるはずです。

おそらく、ファイルを前処理して、各レコードが別個のファイルになるようにする必要があります (または、少なくとも各ファイルが mmap 可能なサイズになるようにします)。

また、次のレコードに進む前に、各レコードのすべての処理ステップを実行していただけますか?おそらくこれにより、IO オーバーヘッドの一部が回避されるでしょうか?

私の考えでは、mmap() を使用すると、開発者が独自のキャッシュ コードを記述する必要がなくなる「だけ」です。単純な「ファイルを一度だけ読み取る」場合、これは難しくありません(ただし、mlbrockが指摘しているように、メモリコピーはプロセススペースに保存されます)が、ファイル内を行ったり来たりする場合、またはビットをスキップするなど、カーネル開発者は次のことを行っていると思います。 おそらく キャッシュの実装は私よりうまくできました...

何年も前に、ツリー構造を含む巨大なファイルをメモリにマッピングしたことを覚えています。ツリーノードの割り当てやポインタの設定など、メモリ内で多くの作業が必要となる通常の逆シリアル化と比較した場合の速度には驚きました。そのため、実際には、MMAP(またはWindowsの対応物)への1回のコールを、オペレーターの新しいコールとコンストラクターコールとの多くの(多くの)コールと比較していました。このような種類のタスクでは、逆シリアル化と比較して mmap は無敵です。もちろん、これについてはブースト再配置可能ポインタを調べる必要があります。

これはマルチスレッドの良い使用例のように思えます...1 つのスレッドがデータを読み取り、他のスレッドがそれを処理するように非常に簡単に設定できると思います。それは知覚されるパフォーマンスを劇的に向上させる方法かもしれません。ちょっとした考え。

mmap の最大の利点は、次のような非同期読み取りの可能性であると思います。

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

問題は、このメモリをできるだけ早くファイルから同期する必要があるというヒントを与える適切な MAP_FLAGS が見つからないことです。MAP_POPULATE が mmap に適切なヒントを提供することを願っています (つまり、呼び出しから戻る前にすべてのコンテンツをロードしようとするのではなく、非同期でロードします。feed_data 付き)。マニュアルでは 2.6.23 以降 MAP_PRIVATE なしでは何も行わないと記載されていますが、少なくともこのフラグを使用するとより良い結果が得られます。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top