※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( FUJIWARA Katsunori )
受託開発主体の独立系ソフトウェアハウス数社を経て、現在フリーランス。
前職で、 HPC ( High Performance Computing ) 系システムのために Solaris 向けファイルシステムを実装したのを機に、 OpenSolaris 勉強会に参加。
「lsを読まずにプログラマを名乗るな!」、 「入門TortoiseHg+Mercurial」とか 「俺のコードのどこが悪い?―コードレビューを攻略する40のルール」、 「アセンブラで読み解くプログラムのしくみ」(電子書籍) といった書籍の執筆や、 技術系ウェブ媒体への記事の寄稿も。
ホームページ以外にも、 はてなダイアリー (id:flying-foozy) 等で情報発信中。 Twitter アカウント (@flyingfoozy) は細々と運用中。
本資料は、 "Solaris Internals" の第13章を、 各段落毎に「ワンフレーズ」化すること基本としています。
「ワンフレーズ」化対象の段落を識別するために、 以下のような識別情報表記を使用します。
[{章/節番号}/{通し段落番号}]@p.{ページ番号}
例えば、"[9.2.6/2]@p.465" は、 以下で始まる 「465 ページに配置されている、9.2.6 節における第2段落」 を指します。
As shown in the example, the program's address space comprises ...
列挙の各項目も1段落として扱います。 単一の列挙項目が複数の段落から構成されている場合は、 構成要素の段落を、それぞれ個別の段落として扱います。
識別情報の構成要素は、 紙媒体の "Solaris Internals Second Edition" を元にしています。
なお、個人的に興味深いと思った点に関しては、 勝手に掘り下げた話を展開します。
書籍の正誤情報は、 Solaris Internals サイトの、 Errata 情報を参照してください。
[13/1]@p.xxx
本章では、 ユーザプロセスにおけるページサイズの拡大による、 潜在的な性能改善の可能性を計測する方策を説明します。
本章で登場するコマンド類:
[13.1/1]@p.xxx
ラージページの使用による、 アプリケーションの性能改善の可否を判定するには、 TLB ミスに関する処理で費やされる、 プロセッサ時間や命令実行数の計測が必要です。 ユーザのアドレス空間で実行中のプロセスで発生した TLB ミスは、 「ユーザ時間」として計上されます。 たとえ CPU 時間の殆どが TLB ミスへの対応 = カーネル内部での処理で費やされても、 統計情報上は「100% 近くがユーザ時間として消費」とみなされます。
[13.2/1]@p.xxx
Solaris では、 (1) 使用可能なページサイズに関する情報を入手する pagesize (1, not 1M) コマンドや getpagesize(3C) API、 (2) ユーザプロセスやカーネルでのページ使用状況を入手する pmap(1, not 1M) や meminfo(2) API、 (3) TLB ミスによる CPU 消費状況を計測することで、 ラージページ使用による性能向上の有無の判断を手助けする trapstat(1M) や cpustat(1M) コマンド等が提供されています。
「使用可能なページサイズ」情報は、 カーネル起動後に変更されることが無い点で、 稼働中プロセス等での実使用状況を採取する機能とは、 少々毛色が違う気がする。 本文のような2分類 (1 + 2 と 3) よりは、 上記の様な3分類の方が、個人的には好みかなぁ。
コマンドの所属セクションに関しては、 あちこちで表記が揺れている模様(笑)
[13.2/2]@p.xxx
TLB ミスで消費される時間の計測には、 (1) 計測した時間当たりの TLB ミス数と、 TLB ミス時の処理コストとの積算で導出する方法と、 (2) TLB ミス処理滞在時間を直接計測する方法 (TLB ミスがソフトウェアで処理される場合限定) があります。 Solaris 8 でも利用可能な cpustat(1M) コマンドは前者、 Solaris 9 から利用可能な trapstat(1M) コマンドは後者に対応します。
[13.2/3]@p.xxx 〜 [13.2/6]@p.xxx
UltraSPARC は TLB ミスをソフトウェアで処理しているので、 例外処理情報を採取する trapstat(1M) コマンドにより、 TLB ミスに関する統計情報も採取可能です。
ラージページの使用や、 大容量 TLB を持つハードウェアへの移行によって見込まれる性能改善は、 trapstat(1M) で計測された TLB ミスによる CPU 消費量で近似できます。
-t オプション付きの trapstat(1M) で得られる TLB ミスの統計情報では、 TLB ミスによる総 CPU 消費率は、 出力の右下済みに表示されます。 統計情報出力は、 TLB 種別 (テキスト/データ) + アドレス空間種別 (ユーザ/カーネル) の組み合わせによって4つに分類されます。
-T オプションを併用することで、 統計情報出力をページサイズ毎で詳細化可能です。 実行例では、TLB ミスは全て 8K ページで発生していることがわかります。
[13.2/7]@p.xxx
実行例では CPU 時間の半分近くが、 TLB ミスの処理で費やされていますから、 これを取り除くことで、 潜在的にはアプリケーションの性能を倍にすることが可能です。 後述する手法の主眼は、 ラージページの使用により、 主にユーザプロセスのヒープ/スタックセグメントにおける、 データ TLB ミスを最小化することにあります。
trapstat(1M) は /dev/trapstat 経由で MMU 情報にアクセスしている。 非 SPARC 系 CPU ではこのデバイスが存在しないので、 trapstat(1M) は即終了してしまう。残念 orz
trapstat(1M) の実装は usr/src/cmd/trapstat/sun4/trapstat.c に、 /dev/trapstat の実装は usr/src/uts/sun4/io/trapstat.c に格納されている。 基本的には ioctl(2) 経由での制御。
[13.2/8]@p.xxx 〜 [13.2/9]@p.xxx
CPU のハードウェアカウンタを読み出す cpustat(1M) を使うことで、 プロセッサにおけるハードウェアイベントでの CPU 消費を計測できます。 trapstat(1M) 導入以前の Solaris 8 では、 CPU のハードウェアカウンタを使う cpustat(1M) によってのみ、 TLB ミスでの CPU 消費を計測できます。
以下の実行例では、 サンプリング時間毎の、実行サイクル数と、TLB ミス回数を、 CPU 毎に採取しています。
[13.2/10]@p.xxx 〜 [13.2/12]@p.xxx
実行結果から、 プロセッサ #0 上のユーザプロセスで、 毎秒 650M サイクルが実行され、 TLB ミスが 3.5M 発生していることがわかります。 UltraSPARC の TLB ミス時の処理は、 最少 50 から最大 300 サイクルを要するので、 3.5M x 50 = 175M サイクル 〜 3.5M x 300 = 1050M サイクルが消費されるものと推測されます。
CPU クロック情報を元に、 TLB ミスが消費する実時間の概算を算出できます。
CPU クロックが 900MHz であることから、 毎秒 900M サイクルが実行されることがわかります。 以上のことから、 TLB ミス時の処理は、 少なくとも 175M/900M ≒ 19% 程度は CPU 時間を消費するであろうと推測されます。 実際の TLB ミスでは、 メモリの読み出しも発生するため、 CPU 時間の消費はより多くなります。
[13.2.1/1]@p.xxx 〜 [13.2.1/4]@p.xxx
pmap(1) が表示する、 対象プロセス中における各マッピング毎のページサイズは、 meminfo(2) による問い合わせで実現されています。
-s 付きの pmap(1) は、 当該プロセスのアドレス空間中のメモリマッピング毎に、 ページサイズを表示します。
先の実行例では、 すべてのマッピングで 8KB ページが使用されています。 参考までに、ppgsz(1) コマンドを使って、 ヒープ領域で 4MB ページを使用する場合の実行例も示します。
meminfo(2) は、 プロセス自身のアドレス空間における、 物理メモリのマッピング状況を問い合わせます。 このシステムコールを使用することで、 プロセスのアドレス空間に割り当てられたページのサイズを、 プログラム上で入手可能です。
[13.2.2/1]@p.xxx 〜 [13.2.2/5]@p.xxx
本節では、 稼働中の Solaris において利用可能なページサイズの情報を得るための、 3つの方法を説明します。
引数なしの pagesize(1, not 1M) は、 稼働中の Solaris におけるデフォルトのページサイズを表示します。 UltraSPARC なら 8KB、x86/x64 なら 4KB です。
-a オプション付きの pagesize(1, not 1M) は、 稼働中の Solaris で利用可能なページサイズを、 全て表示します。 実行例は UltraSPARC 上でのものです。
getpagesize(3C) は、 稼働中の Solaris における、 デフォルトのページサイズの入手に使用されます。
getpagesizes(3C) は、 稼働中の Solaris で利用可能なページサイズを、 全て入手するために使用されます。
[13.3/1]@p.xxx 〜 [13.3/3]@p.xxx
ラージページン利用の有効性が確定したならば、 ラージページを適用する部位を確定する必要があります。 適用対象としては、 ヒープ、スタック、テキストといった素別がありますが、 TLB ミス要因となるアドレス空間の種別等に関しては、 trapstat が提供する情報は多くありません。
多くの場合、 実行される命令コードは、 プロセスやライブラリのテキスト中に存在するので、 命令 TLB (iTLB) ミスは、 プロセスやライブラリのテキストに起因します。 例えば、 Java 仮想マシンのようなケースでは、 ヒープに格納された実行時コンパイル結果が実行されますが、 一般的なアプリケーションの場合、 iTLB ミスの要因は、 まずはプロセス/ライブラリのテキスト領域と推測して構わないでしょう。
データ TLB (dTLB) ミスは、 ヒープ、スタック、初期化済みデータ領域のような、 書き込み可能セグメントや、 テキスト中の読み出し専用データ領域に起因します。
[13.3/4]@p.xxx 〜 [13.3/7]@p.xxx
Solaris のデフォルトページサイズは、 8KB (UltraSPARC) または 4KB (x86) です。 カーネル空間のテキスト/データ領域に、 ラージページが使用されるケースはありますが、 ユーザ空間でのラージページ使用には、 明示的な指定が必要です。
Solaris 2.6 〜 Solaris 8 では、 System V 形式共有メモリの特殊形 - ISM (Intimate Shared Memory) - でのみラージページを使用可能です。 ISM 領域は、 SHM_SHARE_MMU フラグ付きの shmat(2) システムコールにより、 可能であれば 4MB ページで確保されます。 共有メモリの ISM 化により、 TLB ミスが軽減されることで、 Oracle, Informix や Sybase のようなデータベースでは、 多くのケースで 10 〜 20% 程度の性能改善が図れます。
SHM_SHARE_MMU
Solaris 9 では、 ユーザプロセスでラージページを使用する汎用フレームワークの導入と共に、 4MB 以外のラージページが利用できるようになりました。 ppgsz(1) コマンドや mpss.so(1) ライブラリにより、 アプリケーションを改変することなく、 ラージページを使用できます。 アプリケーション内で memctl(2) を使用することで、 ラージページの使用を明示的にカスタマイズすることも可能です。
Solaris 9 のラージページフレームワークでは、 /dev/null をラージページでマッピングすることで、 ヒープ、スタックおよび anonymous メモリをラージページ化できます。
[13.3.1/1]@p.xxx
Solaris 9 から、複数ページサイズサポート (Multiple Page Size Support: MPSS) が導入されました。 ppgsz(1) コマンドや mpss.so.1(1) ライブラリを経由することで、 対象プロセスに代わって memctl(2) によるページサイズ指定が実施されるので、 従来のプログラムを改変することなく、 実行時にページサイズを変更可能です。
[13.3.2/1]@p.xxx 〜 [13.3.2/2]@p.xxx
ppgsz コマンドは、 指定プロセスのヒープ/スタック領域で、 ラージページ使用を推奨するための、 ラッパーコマンドです。 ページサイズの推奨値設定は、 fork(2) の際には引き継がれますが、 exec(2) の際には引き継がれません。 fork/exec で生成されるプロセスに、 ページサイズ推奨値を引き継がせたい場合は、 mpss.so.1(1) ライブラリを使用します。
ヒープ領域に 4MB ページの使用を推奨する場合の ppgsz(1) 実行例を示します。
[13.3.3/1]@p.xxx 〜 [13.3.3/2]@p.xxx
/usr/lib/libmpss.so ライブラリを使うことで、 起動されたプロセスおよびその派生プロセスにおける、 スタックやヒープでのラージページ使用を任意に設定できます。 ppgsz(1) コマンド利用に対する /usr/lib/libmpss.so ライブラリの利点は、 ラージページサイズ指定が exec(2) システムコールを跨いで有効となる点です。 /usr/lib/libmpss.so ライブラリを使う場合、 LD_PRELOAD の設定が必要です。
LD_PRELOAD
/usr/lib/libmpss.so ライブラリが読み込まれた場合、 以下の MPSS 系環境変数に応じて、 推奨ページサイズの設定や、 プロセス毎ページサイズ設定の読み込みが実施されます。
MPSS
セキュリティや、 副作用の排除のために、 exec(2) 時に環境変数を最小化するケースもあるので、 "inherited across exec()" が保証されているとは限らない。
[13.3.3/3]@p.xxx 〜 [13.3.3/5]@p.xxx
以下の例では、 環境変数設定に引き続き実行される全てのプロセスで、 ヒープ領域に 4MB ページの使用が推奨されます。
設定ファイルにより、 特定のプログラム実行でのみ、 ヒープ領域に 4MB ページの使用を推奨することもできます。 以下の例では、 testprog でのみヒープ領域に 4MB ページの使用が推奨されます。
利用可能な環境変数の詳細は、 mpss.so.1(1) のオンラインマニュアルを参照してください。
[13.3.4/1]@p.xxx 〜 [13.3.4/2]@p.xxx
Sun Studio のコンパイラは、 特定のページサイズを要求する実行可能バイナリを作成する、 幾つかのオプションを提供しています。
-xpagesize=n は、 スタック/ヒープ領域の推奨ページサイズを設定します。 n に指定可能な値は、 SPARC では 4K, 8K, 64K, 512K, 2M, 4M, 32M, 256M, 2G, 16G または default、 x86 では 4K, 2M, 4M, 1G または default です。
-xpagesize=n
書籍では、 -xpage 系オプションは SPARC でのみ使用可能な体の記述であるが、 オンラインマニュアル曰く、 x86 環境でのオプション指定は Sun Studio 12 patch 126498-02 以降で、 x86 環境における 1G サポートは Solaris 10 5/08 以降で可能とのこと。
本節の記述の大半は、 cc(1) のオンラインヘルプからの引用な模様。
[13.3.4/3]@p.xxx 〜 [13.3.4/7]@p.xxx
オプションへの指定値は、 対象実行環境の Solaris OS 上で有効なページサイズ = getpagesizes(3C) が返す値でなければなりません。 無効なページサイズ指定は、 実行時に無視されるだけです。 指定ページサイズが尊重される保証もありません。 対象環境のページサイズ使用状況は pmap(1) や meminfo(2) で特定可能です。
-xpagesize オプションは、 コンパイル時とリンク時に指定することで効果を持ちます。 この機能は Solaris7 〜 Solaris8 環境では利用できないものなので、 オプション指定ありでコンパイルされたバイナリは、 Solaris7 〜 Solaris8 環境ではリンクできません。
-xpagesize
-xpagesize=default は、 実行時環境がページサイズを確定します。 値なしの -xpagesize 指定は、 -xpagesize=default と等価です。
-xpagesize=default
-xpagesize 付きのコンパイルは、 libmpss.so.1 の LD_PRELOAD への指定 (+ MPSS 環境変数設定) や、 ppgsz(1) 経由でのコマンド実行と同じ効果を持ちます。
-xpagesize オプション指定は、 -xpagesize_heap と -xpagesize_stack を同値で指定するのと等価です。 -xpagesize により両者に同値を指定することも、 個別のオプション指定で異なる値を指定することもできます。
-xpagesize_heap
-xpagesize_stack
[13.3.4/8]@p.xxx 〜 [13.3.4/11]@p.xxx
-xpagesize_heap=n は、 ヒープ領域の推奨ページサイズを設定します。
-xpagesize_heap=n
-xpagesize_heap=default は、 実行時環境がページサイズを確定します。 値なしの -xpagesize_heap 指定は、 -xpagesize_heap=default と等価です。
-xpagesize_heap=default
-xpagesize_heap 付きのコンパイルは、 libmpss.so.1 の LD_PRELOAD への指定 (+ MPSS 環境変数設定) や、 ppgsz(1) 経由でのコマンド実行と同じ効果を持ちます。
この機能は Solaris7 〜 Solaris8 環境では利用できないものなので、 オプション指定ありでコンパイルされたバイナリは、 Solaris7 〜 Solaris8 環境ではリンクできません。
[13.3.4/12]@p.xxx 〜 [13.3.4/15]@p.xxx
-xpagesize_stack=n は、 スタック領域の推奨ページサイズを設定します。
-xpagesize_stack=n
-xpagesize_stack=default は、 実行時環境がページサイズを確定します。 値なしの -xpagesize_stack 指定は、 -xpagesize_stack=default と等価です。
-xpagesize_stack=default
-xpagesize_stack 付きのコンパイルは、 libmpss.so.1 の LD_PRELOAD への指定 (+ MPSS 環境変数設定) や、 ppgsz(1) 経由でのコマンド実行と同じ効果を持ちます。
[13.3.5/1]@p.xxx 〜 [13.3.5/2]@p.xxx
既存の memcntl(3C) に対して、 当該プロセスからのページサイズ要求を受理できるような拡張が施されているので、 memcntl(3C) を通して必要に応じてラージページを要求できます。
ページサイズ操作の際には、 cmd として MC_HAT_ADVISE を指定します。 この時、 arg 引数は struct memcntl_mha 構造体のアドレスとみなされます。 現時点で mha_cmd フィールドに指定可能な値は3つで、 いずれの場合も、 予約領域 mha_flags は 0 クリア、 mha_pagesize は推奨ページサイズを設定します。
MC_HAT_ADVISE
struct memcntl_mha
mha_cmd
mha_flags
mha_pagesize
[13.3.5/3]@p.xxx 〜 [13.3.5/4]@p.xxx
mha_cmd に MHA_MAPSIZE_VA が指定された場合、 addr 〜 addr + len 領域の推奨ページサイズを設定します。 mha_pagesize には、 サポート対象のページサイズか、 「システムによる自動選択」を意味する 0 を設定します。 addr および len は、 mha_pagesize で境界整合している必要があります。 対象領域内に異なるメモリ保護設定が含まれる場合、 推奨ページサイズ設定は失敗します。 対象領域中に利用不可領域 (hole) が含まれていたり、 MAP_NORESERVE でマップされている場合は、 推奨ページサイズ設定は失敗します。 指定する対象領域は、 単一の大きなマッピング中の一部でも、 複数マッピングに跨っていても構いません。
MHA_MAPSIZE_VA
MAP_NORESERVE
MAP_NORESERVE でのマッピングを許容しないのは、 スワップ領域無しでのラージページ確保は、 データ保護の上で危険、との判断か?
利用不可領域や保護設定の検証は、 valid_usr_range() 中に確認できるが、 MAP_NORESERVE でのマッピングに関するチェックは、 ざっと見た範囲では見当たらない。
valid_usr_range()
[13.3.5/4]@p.xxx 〜 [13.3.5/7]@p.xxx
memcntl(3C) は、 引数での指定条件に合致する、 任意の MAP_PRIVATE による /dev/zero マッピングに適用されます。 ヒープ領域と、 初期スレッドのスタック領域 (実行時に追加されるスレッド領域のは除く) という、 ユーザ空間中の2つの特別な領域は、 特別な対応が必要です。
ヒープ領域は、 brk 領域に隣接する .bss 領域と、 brk 領域自身で構成されます。
2つの特殊領域向けに、個別のコマンド MHA_MAPSIZE_STACK と MHA_MAPSIZE_BSSBRK が定義されています。
MHA_MAPSIZE_STACK
MHA_MAPSIZE_BSSBRK
MHA_MAPSIZE_STACK や MHA_MAPSIZE_BSSBRK が指定された場合、 mha_pagesize にはサポート対象のページサイズか、 「システムによる自動選択」を意味する 0 を設定します。 この要求は、 スタック領域やヒープ領域全体で有効となり、 以降のページ割り当ての際に適用されます。 ページ割り当て済みの既存のスタック領域やヒープ領域に対して、 新規指定のページサイズによる是正が実施される可能性があります。 この際に、既存領域の先頭アドレスや領域長の境界整合のために、 新規セグメントが生成されるかもしれません。
[13.3.5/8]@p.xxx 〜 [13.3.5/9]@p.xxx
アプリケーションは、 性能を最大化するための境界整合確保 (e.g. マッピング生成時の mmap(2)) や、 ページ細分化を防ぐための境界不整合な操作 (e.g. mprotect(), munmap(), mmap()) の回避について、 熟知している必要があります。
mmap(2) を使用する通常のアプリケーションは、 addr 引数に NULL を指定することで、 OS による任意領域割り当てを許可しますが、 memcntl(2) によるラージページ使用を指示する場合は、 ラージページ使用に必要な境界整合のために、 MAP_ALIGN フラグ指定が必要です。 MAP_ALIGN が指定された場合、 mmap(2) の addr 引数は境界整合値とみなされ、 ユーザ空間中の空き領域から妥当な領域が割り当てられます。 この際の addr 値は、 利用可能ページサイズの 2 の累乗か、 「システムによる自動選択」を意味する 0 を設定します。 MAP_ALIGN が MAP_FIXED と併用された場合や、 境界整合要求を満たす空き領域がない場合、 mmap(2) の実行は失敗します。
MAP_ALIGN
MAP_FIXED
MAP_ALIGN と MAP_FIXED の併用が許されていないのは、 addr が2つの意味を持ってしまうため。
[13.3.5/10]@p.xxx
ヒープ領域の推奨ページサイズを 4MB に設定するサンプルを提示します。 ヒープ領域の先頭アドレスは 4MB で境界整合していないので、 最初の数MBは 8KB ページ上で確保され得ます。 性能に影響するデータがこの領域に配置された場合、 当該プログラムはラージページ使用の恩恵を十分に受けられないかもしれません。 サンプルプログラムのように、 最初のメモリ割り当てに 4MB 境界整合を指定することで、 以降に割り当てられる領域がラージページ上に配置される可能性が高まります。
[13.3.6/1]@p.xxx 〜 [13.3.6/4]@p.xxx
TLB の構成は UltraSPARC プロセッサ毎に大きく異なりますが、 多少は共通点があります。 UltraSPARC I 〜 IV は4つのページサイズ (8KB, 64KB, 512KB, 4MB) をサポートし、 命令/データで個別の TLB を持ちます (以下、特に言及がないものは「フルアソシエイティブ」)。
UltraSPARC I/II は、 iTLB/dTLB 共に「64 エントリ, 全ページサイズ対応」を1つずつの、 合計2つの TLB を持ちます。
UltraSPARC III (750MHz) は、 iTLB が 「16 エントリ, 全ページサイズ対応」と 「128 エントリ, 8KB ページ対応」の2つ。 dTLB が 「16 エントリ、全ページサイズ対応」と 「2-way, 512 エントリ, 8KB ページ対応」の2つの、 合計4つの TLB を持ちます。 dTLB の全ページサイズ対応の 16 エントリは、 カーネル領域向けにロックされた 9 エントリと、 ユーザ空間で使用可能な 7 エントリで構成されるため、 このプロセッサ上でのラージページによる恩恵は、 それほど多くありません。
UltraSPARC III (900MHz 以上) は、 iTLB が 「16 エントリ、全ページサイズ対応」と 「128 エントリ, 8KB ページ対応」の2つ、 dTLB が 「16 エントリ、全ページサイズ対応」を1つと、 「2-way, 512 エントリ, 1ページサイズ/プロセス」を2つの、 合計5つの TLB を持ちます。
[13.3.6/5]@p.xxx 〜 [13.3.6/8]@p.xxx
xxxx
大容量 TLB 側は、 4種類のページサイズ全てに対応しますが、 プロセス毎には一種類のページサイズしか許容されないので、 非ラージページマッピングが使用する 8KB ページと、 ラージページマッピングが使用するページサイズの、 合計2種類がプロセス毎に同時使用されます。 TLB による網羅範囲が広い、 最も一般的なページサイズ選択は、 8KB と 4MB の組み合わせです。
x86 は、 4KB ページのみサポートします。
AMD64/x64 は、 4KB および 2MB ページをサポート