この記事は、インテル® デベロッパー・ゾーンに公開されている「Code Sample: Implement a Persistent Memory Cache-A Simple Find Example」の日本語参考訳です。
この記事の PDF 版はこちらからご利用になれます。
ファイル: | ダウンロード (英語) |
ライセンス: | 3-Clause BSD License (英語) |
動作環境 | |
---|---|
OS: | Linux* カーネル 4.3 以上 |
ハードウェア: | インテル® Optane™ DC パーシステント・メモリーと 第 2 世代インテル® Xeon® スケーラブル・プロセッサー エミュレート: 「DRAM を使用して不揮発性メモリーをエミュレートする方法」 (英語) を参照 |
ソフトウェア: (プログラミング言語、ツール、 IDE、フレームワーク) |
C++ コンパイラー、不揮発性メモリー開発キット (PMDK) ライブラリー |
必要条件: | C++ と PMDK の使用経験 |
はじめに
この記事では、不揮発性メモリーをキャッシュとして利用することで、ユーザー体験を向上する方法を示します。不揮発性メモリーを使用することでデータモデルを 1 カ所にまとめ、メインメモリーとストレージの間でデータを分割する必要がなくなります。
不揮発性メモリー向けのコードには、不揮発性メモリー開発キット (PMDK) (英語) に含まれる高レベルのトランザクション・ライブラリーである、libpmemobj ライブラリーの C++ バインディング (英語) を使用します。詳細は、インテル® デベロッパー・ゾーンの不揮発性メモリー・プログラミング (英語) を参照してください。導入に必要な情報を入手できます。
この記事で紹介するコードはすべてこちらのリポジトリー (英語) にあります。手順に従ってコンパイルしてください。
揮発性バージョン
このサンプルコードは、find-all と呼ばれる Linux* の find コマンドの簡易バージョンに対応しています。上記のリポジトリーにアクセスすると、このサンプルには create_test_tree.sh スクリプトが用意されているのが分かります。このスクリプトは、次の 3 つのパラメーターを使用して任意のディレクトリー構造を作成します。
tree-root: ディレクトリーのルートの位置。
tree-depth: ツリーに含まれるサブディレクトリーのレベルの数。
files-per-level: 各レベルで作成されるファイルの数。
作成されるファイルは空です (find コマンドは名前でファイルを検索するため、ここではファイル名のみ必要です)。各レベルのサブディレクトリーの数は 3 に固定されています。
例えば、ルートが ./test_tree で、6 つのレベルがあり、各レベルに 100 ファイルがあるディレクトリー・ツリーを作成する場合、次のコマンドを実行します。
$ ./create_test_tree.sh test_tree 6 100
ルートをリストすると、次のような出力になります。
[sdp@localhost find_all_pm]$ ls test_tree/ acvlhpknswalwbwtnpzggaewqnacpopv httjlfkrsbjvnydfwkfiekfqkbqtstkq rgwbefyncedkyvpziijfqgzxcghcbyua agocrfeblfzeirldzyzyqeqsqecwodio hxuimxojjfprdmuoqxkeopavhjuldomd rhafepwdyvoxoxhhfqyybajfhkijkryh bdpdfdfysqkpilptzfqypldeydiemcel idvcepykqyvwuthshsopckxkpuxqmdab ... [sdp@localhost find_all_pm]$
ファイル名は 32 文字で、ランダムに生成されます。ディレクトリー名もランダムに生成されますが、5 文字です。ここでは、364 サブディレクトリーと 36,400 ファイルを生成しました。
これで、任意の文字列を含むファイル名を検索する準備ができました。ファイル名に「aazz」という文字列を含むファイルを検索します。
$ ./find-all ./test_tree aazz ./test_tree/gzqtl/kwntu/fvecf/tivvl/kuzpl/klwfgiwqenljruorfdupgkmaazzydrik ./test_tree/gzqtl/kwntu/opuci/ktxmi/chisz/enlvjaazzuanuguupnnrrwxgfafekwck ./test_tree/hfibr/fifpu/wtpdt/mgrex/mbrma/sapjhqtpnxaazzkflhbmgtrhqbcxkvuj $
次のように、3 つのファイルが見つかりました。検索にかかった時間を確認します。
$ time ./find-all ./test_tree aazz ./test_tree/gzqtl/kwntu/fvecf/tivvl/kuzpl/klwfgiwqenljruorfdupgkmaazzydrik ./test_tree/gzqtl/kwntu/opuci/ktxmi/chisz/enlvjaazzuanuguupnnrrwxgfafekwck ./test_tree/hfibr/fifpu/wtpdt/mgrex/mbrma/sapjhqtpnxaazzkflhbmgtrhqbcxkvuj real 0m0.579s user 0m0.517s sys 0m0.060s
* パフォーマンスに関する注意事項を参照してください。
約 0.5 秒です。わずかのように見えますが、検索クエリーを頻繁に実行する必要がある場合は数秒になる可能性があります。さらに、ツリーの構造が複雑になると (以下を参照)、応答時間が数秒になり、ユーザー体験が大きく損なわれる可能性があります。
不揮発性バージョン
このバージョンでは、各パターンのディレクトリー構造 (ツリー) を模倣する永続的なデータ構造が追加されます。データ構造は、ルート・オブジェクトで始まります。ルートには、ユーザーが以前に検索したパターン のリストがあります。
class root { private: pobj::persistent_ptr<pattern> patterns; public: pattern *find_pattern(const char *patstr, const char *rootstr) { ... } pattern *create_pattern(const char *patstr, const char *rootstr) { ... } };
各パターンから、すべてのサブディレクトリーを格納するツリーを作成します。ファイルは、データ構造の効率を維持するため、以前のクエリーに一致するファイル名のみ格納します。
ツリー内の各サブディレクトリーには、最後に検索が実行された時間も格納されます。ユーザーが同じパターンを検索すると、プログラムは単純にこのインメモリー・ツリー構造を反復します。ファイルシステム内の特定のサブディレクトリーの変更時間が最後の検索から変わっていない場合 (変更時間は、サブディレクトリーに新しいファイルやディレクトリーが追加または削除された場合にのみ更新されます)、そのサブディレクトリーは再スキャンする必要がありません。単純にキャッシュされている結果を出力して、次のサブディレクトリーへ再帰的に移動できます。以下は、class pattern のコードです。
class pattern { private: pobj::persistent_ptr<char[]> patstr; pobj::persistent_ptr<char[]> rootstr; pobj::persistent_ptr<entry> rootdir; pobj::persistent_ptr<pattern> next; public: pattern(const char *patstr, const char *rootstr) { NEW_PM_STRING(this->patstr, patstr); NEW_PM_STRING(this->rootstr, rootstr); this->rootdir = pobj::make_persistent<entry>(nullptr, rootstr, true); this->next = nullptr; } const char *get_patstr(void) { return this->patstr.get(); } const char *get_rootstr(void) { return this->rootstr.get(); } pobj::persistent_ptr<pattern> get_next(void) { return this->next; } void set_next(pobj::persistent_ptr<pattern> pat) { this->next = pat; } int find_all(void) { return rootdir->process_directory(this->patstr.get()); } };
関数 find_all() は、パターンでツリールートをスキャンするために呼び出されます。この関数は、特定のディレクトリー・ルート以下 (最初のエントリーは常にツリーのルートであるため) のすべてのファイルとディレクトリーをスキャンする class entry から再帰関数 process_directory() を呼び出します。
エントリーは、ディレクトリーまたはファイルです。ファイルエントリー (以前の検索でキャッシュされた) は単純に結果として出力されます。一方、ディレクトリーは process_directory() を呼び出して再帰的に処理されます。前述のとおり、最後の検索からディレクトリーの変更時間が変わっている場合は、その内容をファイルシステムから再スキャンする必要があります。
... /* Let's get current 'last modif time' */ stat(path, &st); time_t new_mtime = st.st_mtime; if (difftime(new_mtime, this->mtime) != 0) { /* dir content has changed, we need * to re-scan it * */ while ((dirp = readdir(dp)) != NULL) { ...
以下は class entry の変数です。
class entry { private: pobj::persistent_ptr<char[]> parent; pobj::persistent_ptr<char[]> name; pobj::p<bool> isdir; pobj::p<time_t> mtime; pobj::persistent_ptr<entry> entries; pobj::persistent_ptr<entry> next; ... };
永続プログラムの名前は find-all-pm です。実行するには、各種パラメーターに加えて、不揮発性メモリープールの場所を指定する必要があります (プールが存在しない場合はプログラムによって作成されます)。ここでは、/mnt/pmem/pool にプールがあります。
$ time ./find-all-pm /mnt/pmem/pool ./test_tree aazz ./test_tree/hfibr/fifpu/wtpdt/mgrex/mbrma/sapjhqtpnxaazzkflhbmgtrhqbcxkvuj ./test_tree/gzqtl/kwntu/opuci/ktxmi/chisz/enlvjaazzuanuguupnnrrwxgfafekwck ./test_tree/gzqtl/kwntu/fvecf/tivvl/kuzpl/klwfgiwqenljruorfdupgkmaazzydrik real 0m0.699s user 0m0.020s sys 0m0.038s
* パフォーマンスに関する注意事項を参照してください。
結果から、新しいパターンで初めて実行すると実行時間は長くなることが分かります。ここでは、揮発性バージョンと比較して 0.120 秒遅くなりました (0.579 秒と 0.699 秒)。しかし、再度実行すると、次のような結果になります。
$ time ./find-all-pm /mnt/pmem/pool ./test_tree aazz ./test_tree/hfibr/fifpu/wtpdt/mgrex/mbrma/sapjhqtpnxaazzkflhbmgtrhqbcxkvuj ./test_tree/gzqtl/kwntu/opuci/ktxmi/chisz/enlvjaazzuanuguupnnrrwxgfafekwck ./test_tree/gzqtl/kwntu/fvecf/tivvl/kuzpl/klwfgiwqenljruorfdupgkmaazzydrik real 0m0.058s user 0m0.020s sys 0m0.038s
* パフォーマンスに関する注意事項を参照してください。
50 ミリ秒しかかかっていません。つまり、揮発性バージョンは 10 倍低速です。
大きなツリー
大きなツリーでテストしてみます。ファイルの数を 10 倍に増やしてみましょう。
$ ./create_test_tree.sh test_tree1 6 1000
揮発性バージョンを実行します。
$ time ./find-all test_tree1 aazz test_tree1/npkhu/egzez/qyixsuqzfywemoaazzgdwququodezchy test_tree1/npkhu/egzez/fpvee/sbkue/vupmb/xptjsqiqtuchcspywsjaazzxceuaokfa test_tree1/npkhu/egzez/fpvee/zrrhb/agera/hcxhbztjmfmedzbytkgdwxeaazzygnnp test_tree1/mgwwl/tsjzb/xuojd/wsjzw/iittr/icaazzzwzmdaevemdkjsybtegxccrjqq ... real 0m5.531s user 0m5.042s sys 0m0.476s
* パフォーマンスに関する注意事項を参照してください。
5.5 秒で結果が見つかりました。不揮発性バージョンを実行します。
$ time ./find-all-pm /mnt/pmem/pool test_tree1 aazz test_tree1/mgwwl/tsjzb/xuojd/wsjzw/iittr/icaazzzwzmdaevemdkjsybtegxccrjqq test_tree1/mgwwl/tsjzb/xuojd/isplz/jlqje/gwalshqfqlopanbutlcduuaazznziwle test_tree1/mgwwl/tsjzb/xuojd/isplz/fpcpaectvcipoaazzuhvfltrcrxrqvnz test_tree1/mgwwl/tsjzb/thybd/elaga/uczjzwoywatubaazzcktnsmlfvgbxoal ... real 0m5.568s user 0m5.001s sys 0m0.531s
* パフォーマンスに関する注意事項を参照してください。
揮発性バージョンとほぼ同じで、5.7 秒かかりました。これは、このプログラムの主なボトルネックが不揮発性メモリーへの書き込みではないことを示しています。むしろ、ファイルシステムからのファイルのメタデータの読み取りとパターンマッチングの可能性があります。
同じパターンを再度検索します。
$ time ./find-all-pm /mnt/pmem/pool test_tree1 aazz test_tree1/mgwwl/tsjzb/xuojd/wsjzw/iittr/icaazzzwzmdaevemdkjsybtegxccrjqq test_tree1/mgwwl/tsjzb/xuojd/isplz/jlqje/gwalshqfqlopanbutlcduuaazznziwle test_tree1/mgwwl/tsjzb/xuojd/isplz/fpcpaectvcipoaazzuhvfltrcrxrqvnz test_tree1/mgwwl/tsjzb/thybd/elaga/uczjzwoywatubaazzcktnsmlfvgbxoal ... real 0m0.058s user 0m0.022s sys 0m0.036s
* パフォーマンスに関する注意事項を参照してください。
以前と同様に 58 ミリ秒になりました。相対的には、揮発性バージョンはおよそ 100 倍低速です。一般に、ツリーに含まれるファイルの数が多くなるにつれて、不揮発性メモリーキャッシュの使用によってもたらされる利点は大きくなります。
まとめ
この記事では、不揮発性メモリーをキャッシュとして利用することで、ユーザー体験を向上する方法を紹介しました。具体的には、不揮発性メモリーキャッシュを使用して、Linux* の find コマンドの簡易バージョンに対応する小さなサンプルコードのパフォーマンスを 36,400 ファイルでは 10 倍、364,000 ファイルでは 100 倍向上しました。この記事で紹介したコードはすべてこちらのリポジトリー (英語) にから入手できます。
法務上の注意書き
* パフォーマンス結果は 2019 年 3 月 1 日時点のテスト結果に基づいたものであり、公開されている利用可能なすべてのセキュリティー・アップデートが適用されていない可能性があります。詳細については、構成の開示を参照してください。絶対的なセキュリティーを提供できる製品はありません。性能に関するテストに使用されるソフトウェアとワークロードは、性能がインテル® マイクロプロセッサー用に最適化されていることがあります。SYSmark* や MobileMark* などの性能テストは、特定のコンピューター・システム、コンポーネント、ソフトウェア、操作、機能に基づいて行ったものです。結果はこれらの要因によって異なります。製品の購入を検討される場合は、他の製品と組み合わせた場合の本製品の性能など、ほかの情報や性能テストも参考にして、パフォーマンスを総合的に評価することをお勧めします。
システム構成: 2019 年 5 月 10 日時点のインテルによるテスト。1 ノード、2x インテル® Xeon® Platinum 8260 プロセッサー、Wolfpass プラットフォーム、合計メモリー 192GB (12 スロット/16GB/2667MT/秒 DDR4 RDIMM)、合計不揮発性メモリー 1.5TB (12 スロット/128GB/2667MT/秒) インテル® Optane™ DC パーシステント・メモリー・モジュール (DCPMM)、インテル® ハイパースレッディング・テクノロジー有効、ストレージ (ブート): 1x TB P4500、ucode: 0x400001C、OS: CentOS* Linux* 7.6、カーネル: 3.10.0-957.12.2.el7.x86_64
次の脆弱性に対するセキュリティー緩和策: CVE-2017-5753、CVE-2017-5715、CVE-2017-5754、CVE-2018-3640、CVE-2018-3639、CVE-2018-3615、CVE-2018-3620、CVE-2018-3646、CVE-2018-12126、CVE-2018-12130、CVE-2018-12127、CVE-2019-11091
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。