※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( FUJIWARA Katsunori )
受託開発主体の独立系ソフトウェアハウス数社を経て、現在フリーランス。
前職で、 HPC ( High Performance Computing ) 系システムのために Solaris 向けファイルシステムを実装したのを機に、 OpenSolaris 勉強会に参加。
「lsを読まずにプログラマを名乗るな!」、 「入門TortoiseHg+Mercurial」とか 「俺のコードのどこが悪い?―コードレビューを攻略する40のルール」、 「アセンブラで読み解くプログラムのしくみ」(電子書籍) といった書籍の執筆や、 技術系ウェブ媒体への記事の寄稿も。
ホームページ以外にも、 はてなダイアリー (id:flying-foozy) 等で情報発信中。 Twitter アカウント (@flyingfoozy) は細々と運用中。
本資料は、 "Solaris Internals" の第16章を、 各段落毎に「ワンフレーズ」化すること基本としています。
「ワンフレーズ」化対象の段落を識別するために、 以下のような識別情報表記を使用します。
[{章/節番号}/{通し段落番号}]@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 情報を参照してください。
[16/1]@p.xxx 〜 [16/3]@p.xxx
Sun が提供する旧来のシステムでは、 機器上の全てのメモリが、 どの CPU コアからも (遅延時間上) 等距離に見える SMP (Symmetric Multi Processor) 構成でした。 その上、同一機器上の共有要素は、 全ての CPU から共有されていました。
新しいシステムでは、 上記の単純なシステムから2つの点で異なっています。 一つ目の差異は、 CPU によってメモリとの「距離」が異なっている点です。 この特性は、Non Uniform Memory Access (NUMA) として知られるものです。 二つ目の差異は、 複数の(論理) CPU 間で、 処理機能要素やキャッシュを共有している点です。 この特性をここでは Chip MultiThreading (CMT) と呼びます。
Solaris では、 メモリ配置最適化 (Memory Placement Optimization: MPO) や CMT 最適化を行うことで、 cache coherent NUMA (ccNUMA) のような非対称メモリ階層を持つシステムや、 チップレベルでマルチスレッド/マルチプロセス処理を行うシステムを、 サポートしています。 メモリの局所性 (各CPUとメモリの間の「距離」) や、 論理 CPU 間での機能要素 (キャッシュ/データ経路等) の共有状況を把握することで、 Solaris は NUMA/CMT システム上での性能を最適化できます。
[16.1/1]@p.xxx
本節では、NUMA や CMT に関して、 その性質や必要性について詳しく見ていきます。
[16.1.1/1]@p.xxx
NUMA なシステムは、 小規模で高速なローカルバスによって相互接続された CPU やメモリ、(場合によっては) I/O デバイスから構成される「ノード」(node)を、 バスによって複数接続したものです。 相互接続することで、 同一の物理アドレス空間を共有し、 各 CPU はシステム上の全てのメモリにアクセス可能ですが、 ローカルノード上のメモリへのアクセスと比較して、 別ノード (remote node) 上のメモリへのアクセスは、 時間を要します。 別ノードアクセスに要する遅延度合いは、多岐にわたります。 各ノードに対応する物理的なボードを複数収容するシステムもあれば、 メモリが搭載された複数の (CPU) チップで構成されるシステムもあるでしょう。
2011 年頃だと、 富士通は複数システムボード構成 (SPARC Enterprise M5000 あたり?) にならない限りは、 複数ソケット構成でも UMA で頑張る系だったが、 今でもそうなのかな?
[16.1.1/2]@p.xxx 〜 [16.1.1/3]@p.xxx
別ノード上のメモリアクセス性能は、 相互接続経路の転送速度、 ノード間接続の経路構成 (topology)、 メモリキャッシュの有無 (+ どこにキャッシュされているか)、 キャッシュ整合性 (coherency) 管理の有無、 といった要素に影響を受けます。 例えば、 Startcat システムにおける、 別ノード上のメモリからの非キャッシュ時読み出しは、 ローカルノード上のそれと比較して、 1.5 〜 3 倍程度の時間を要します。
Startcat や、AMD Opteron の Hypertransport ベースのシステムなどは、 NUMA 構成の良い例です。
Wikpedia 曰く:
最大で、 UltraSPARC III x 106 または UltraSPARC IV x 72 の CPU を、 18 毎のボードに収容
Hypertransport ベースの AMD Opteron システムは、 CPU パッケージ毎にメモリコントローラを内臓するから、 複数ソケット構成だと必ず NUMA になる、 という話だったような記憶が……
一方 Nehalem 前の Intel CPU は、 複数ソケット構成でも、 外部チップセット上のメモリコントローラを共有する形態があるので、 複数ソケット = NUMA 構成とは限らかった筈。
[16.1.1.1/1]@p.xxx 〜 [16.1.1.1/2]@p.xxx
大量の高速な CPU から SMP システムを構成する場合、 全ての CPU や I/O 等々を接続する必要がありますが、 バックプレーンの処理能力は物理サイズによって制約されますし、 CPU の量/性能の増加によって常に帯域占有の圧力にさらされます。
SMP システムの性能問題解決のために、 カード接続の双方向化 (centerplace)、 クロスバースイッチによる相互接続 (アクセスの並列化)、 バス幅の拡張等々の対応がとられましたが、 継続的な性能向上のためには、 異なるアプローチが必要とされました。 NUMA によって、 単一 SMP 構成を超えることができます。
centerplane って初めて聞いた (^ ^ ;;;) 「双方向差し込み可能な基盤」が筐体中央に配置されるから center なのね。
[16.1.1.1/3]@p.xxx 〜 [16.1.1.1/4]@p.xxx
但し NUMA は、 設計上の妥協として、 相当量の遅延を許容しています。 NUMA 構成のシステムでは、 あるスレッドの実行におけるメモリアクセスの遅延量は、 スレッドが実行される位置 (ノード/CPU)、 アクセス対象メモリの位置といった様々な要因の影響を受けるため、 アプリケーションの実行性能は非決定的です。 「複数スレッドによる実行が一定時間内に完了する」仮定の元で、 アプリケーションの実行性能を想定したい場合、 この特性は非常に問題があります。
マシンの性能を最大限引き出したい人々にとっては、 プロセッサ〜メモリ〜I/O の接続経路構成に配慮した最適化が必要となります。 しかし、特定環境への最適化は、 実装の可搬性低下や、試験/保守コストの増加を招くため、 これまでの Solaris への新規 API 採用の経験上、 このような最適化のためのソフトウェア改変は厭われることがわかっています。 そこで Solaris では、 任意のシステム構成上で有効なスレッドとメモリ間の関連性を、 カーネルに知らせてあげる API を提供しています。 新規 API を用いた実装の改変を好まない(or 改変できない)場合でも、 規定のポリシー群を適用することで、 Solaris カーネルがアプリケーションの最適化を行います。
後の節でも言及されているが、 「突発的なピーク性能の高さ」よりは、 多少性能が劣化しても「一定性能が安定的に再現可能」 であることを重視しているあたりが、 とても実用本位な感じ(笑)
[16.1.1.2/1]@p.xxx
SUN を含めた NUMA 構成のシステムを開発しているベンダにとっては、 Cache Coherent NUMA (ccNUMA) は NUMA とほぼ同義です。 ccNUMA 構成では、 機器上の複数ノードに跨るキャッシュの一貫性を、 ハードウェアレベルで保守します。 ソフトウェアによる一貫性保持や、 ノードを横断するキャッシュ保持の禁止といった代替手法と比較して、 ccNUMA は非常に高速です。 Solaris における MPO 対応は ccNUMA 上においてのみ有効です。
暫く前に Intel が、 「Cache Coherency 層の実装をソフトウェア化することで、 超多重コアを実現」的なデモを行っていたニュースがあった気がするが、 このシステムの場合、 「Cache Coherency 層の実装」は、 カーネルから見える (or カーネルが一枚噛んでいる) イメージなんだろうか? あるいは「シリコンに焼いていない」だけで、 カーネルから見えるような位置には無い?
そもそも、 事後的に OS からエラッタ修整のマイクロコードを流し込める今時の CPU の場合、 MMU とか Cache Coherency 層とかも、 厳密には「(改変不可な) ハードウェア実装」じゃなかったりする?
[16.1.2/1]@p.xxx 〜 [16.1.2/3]@p.xxx
Chip MultiThreading (CMT) とは、 単一物理プロセッサ上で、 複数のスレッドを同時に (simultaneously) 実行する技術を指します。 CMT には、幾つかの実現方式があります。
一つ目の実現方式は、 複数の処理コア (processing core) を単一物理パッケージに納めた Chip MultiProcessing (CMP) です。 UltraSPARC IV は、 2つの UltraSPARC III+ を単一物理パッケージ化したもので、 Sun 初の CMP チップです。
二つ目の実現方式は、 単一処理コアのパイプラインを多重化 (multiplex) することで、 複数スレッドの実行を行う Vertical multiThreading (VT) です。 メモリアクセス待ちでパイプラインを空けるよりは、 別スレッドの実行に切り替えてしまう手法です。 VT はハードウェアで実現されるので、 OS からは複数の論理 CPU に見えます。 VT による CMP 実装例としては、 Sun の UltraSPARC T1 (Niagara) があげられます。 8 コアを搭載し、 コア当たり 4 スレッドが実行可能なので、 OS からは 32 個の論理 CPU に見えます。
[16.1.2/4]@p.xxx 〜 [16.1.2/5]@p.xxx
Simultaneous MultiThreading (SMT) は、 単一処理コアが複数スレッドを実行する点では VT と同じですが、 複数の命令を同時に実行できる点で VT とは異なります。
SMT プロセッサの例としては、 Pentium4 や Xeon があげられます。
結局のところ、 処理コアあたりの命令実行ユニットが1つか複数かの違い? > VT/SMT
「物理 N コア!」 by AMD とか、 「論理的には N コア!」 by Intel とかの、 「(完全 N 並列実行とは言ってない)」な括弧書き付きな迷走時期を思い出すに、 この辺は各社結構いい加減な気も…… (^ ^ ;;;) マーケティング主導な用語?(笑)
[16.1.2.1/1]@p.xxx 〜 [16.1.2.1/3]@p.xxx
CMT における性能向上手法は、 単一プロセッサアーキテクチャにおける従来のそれとは異なるものです。
CPU の処理性能と、メモリ性能が開くことで、 メモリアクセス待ちに要する時間が支配的になってしまい、 クロックアップによる性能向上の効果が低下してきました。 CMT は、 メモリアクセス待ちで空いてしまったパイプラインを、 有用な別の処理で埋めてしまおうというものです。
CMT の主眼は、 単一命令列の実行性能向上ではなく、 複数スレッドの同時実行にりよる、 単一時間あたりの総命令実行性能 (= スループット) の向上にあります。 VT や SMT での並列性は、 パイプラインの空きを別の命令実行列 (=スレッド) で埋めることで実現されます。 CMP での並列性実現では、 更なる処理コアのチップへの追加という手法も含まれます。
単純に複数処理コアを単一パッケージ化しても、 メモリコントローラを CPU パッケージ単位で保持しないと、 結局「複数 CPU での SMP 構成」問題にぶち当たる気が…… > CMP
[16.1.2.2/1]@p.xxx 〜 [16.1.2.2/3]@p.xxx
CMT サポートがない場合、 利用可能な個々の論理 CPU は、同一の特性を持つものとしてしか扱えません。 例えば、 論理 CPU 間でのパイプラインの共有や、 キャッシュの共有、 キャッシュやメモリへのアクセス経路共有などで、 利用可能な論理 CPU がグループ化されているようなケースでは、 カーネルがハードウェア構成を把握できることが重要になります。 各スレッドをどの論理 CPU 上で実行するかが、 性能に影響するためです。
Solaris の CMT サポートは、 論理 CPU 間の関連性を把握することで、 スレッド実行をシステム全体に均衡化し、 資源共有における競合回避や、 帯域利用の向上を図ります。 例えば、 複数の論理 CPU がキャッシュを共有しているケースでは、 スレッドを実行する論理 CPU を変更する際に、 キャッシュを共有している論理 CPU の中から選択した方が、 性能向上に寄与します。
例えば大量のメモリ帯域を必要とする複数のスレッドがある場合、 個々のスレッドをメモリコントローラが異なる物理 CPU 上で実行できた場合、 スレッド間での資源 (= メモリコントローラ) 競合は回避できますが、 カーネルがハードウェア構成を認知できない場合、 同一メモリコントローラを共有する論理 CPU 上で実行してしまうかもしれません。
[16.2/1]@p.xxx 〜 [16.2/5]@p.xxx
以下の3つの概念によって、 Solaris は NUMA 構成で性能の最適化ができるようになっています。
NUMA 構成システム上でアプリケーションを効率良く実行する上で、 CPU, キャッシュ, I/O 等の実行時に必要とされる資源が、 co-locate されることが重要です。 co-locate されることで、 メモリアクセスの大部分が局所化され、 別ノード上のメモリへのアクセスによる遅延を回避できます。 Solaris カーネルの MPO 対応では、 局所性を認識可能にすることで co-locate させています。 どの資源がどのノード上にあるのか (= 局所性) を把握することで、 アプリケーションの実行に必要な資源の割り当ての際に、 性能に配慮して、より近傍の資源を割り当てることができます。
辞書やら用語解説的には、 co-location = 複数機能/サービスでの資源共有 (e.g. ハウジングサービス) といった、「相乗り」的なニュアンスなのだが、 そうなると原文の解説とはちょっと違う感じになる気が……
[16.2/6]@p.xxx 〜 [16.2/8]@p.xxx
先述した最適化に加えて、Soalris カーネルは、 資源配置の経路構成情報取得や配置制御に関する I/F を、 アプリケーション層に提供しています。
局所性が重要である一方で、 実行時の局所性が高くなりすぎると、 少数資源に多数のスレッドが殺到することになります。 カーネルが負荷の均衡を図ることで、 特定の資源の過負荷状態を回避します。 Solaris の MPO フレームワークは、 実行中のハードウェア構成変更にも追従します。
プロセススケジューリングや資源配置に関して、 最適な判断を下すために、 カーネルはハードウェア構成を認識できる必要があります。 実際の物理構成との厳密な対応はともかく、 Solaris では簡単なモデルでハードウェア構成を表現します。
[16.2.1/1]@p.xxx 〜 [16.2.1/4]@p.xxx
遅延モデルは、1つないし複数の locality group (lgroup) から構成されます。 locality group は、 システム上の全ての資源を、 近しい資源単位で纏め上げたもので、 以下の様な要素で構成されます。
このモデルは、 AMD Opteron や Starcat をはじめとする NUMA 構成システムにおける、 メモリアクセス時遅延の挙動を適切に取り扱えます。 例えば Starcat の場合、 各物理ボード上に、 複数の CPU とローカルメモリが実装され、 同一物理ボード上のメモリアクセスは、 別物理ボード上のそれへのアクセスよりも高速です。 この場合、 物理ボード毎に lgroup を割り当てることで、 十分表現できます。
[16.2.2/1]@p.xxx 〜 [16.2.2/3]@p.xxx
もっと複雑な構成の場合、 2段階よりも多い遅延レベルになります。 例えば AMD Opteron の 4 CPU 構成の場合、 経路構成はリング状になります。
この場合、 (1) 各 CPU のローカルメモリ、 (2) 一段階離れた CPU に接続されたメモリ、 (3) 更に一段階離れた CPU に接続されたメモリ、 という三段階のメモリアクセス遅延が存在します。 8 CPU 構成の場合、 通常は図 16.3 のような梯子状の構成になります。
Solaris カーネルは、 各 CPU に応じた lgroup を作成し、 資源の近傍性を容易に確認できるようにします。
[16.3/1]@p.xxx 〜 [16.3/2]@p.xxx
スレッド生成時に、 「ホーム lgroup」 (home lgroup) が設定されます。 各スレッドの実行やメモリ割り当てを、 ホーム lgroup を中心に行うことで、 スレッド実行における局所性が確保できます。 ホーム lgroup の選定では、 プロセス中のスレッド数、 プロセス中のスレッドの lgroup 分散性、 システム中の各 lgroup のサイズや負荷状況が考慮されます。
各スレッドのホーム lgroup の変更には、 2つのケースがあります。 もっとも端的なケースは、 オフライン化や dynamic reconfigure 操作によって、 ホーム lgroup に属する CPU がなくなった場合です。 もう一つのケースは、 スレッドが別の lgroup に属する CPU に固定された場合です。 この場合、固定先 CPU の属する lgroup が新たなホーム lgroup になります。 なお、 CPU への固定が解除されても、 ホーム lgroup 設定はそのままです。
[16.4/1]@p.xxx 〜 [16.4/4]@p.xxx
スレッドは、 可能な限りホーム lgroup 上の CPU で実行されます。 当該 lgroup 上の CPU がすべて利用中で、 且つ実行中のスレッドの実行優先度が当該スレッドのものよりも高い場合は、 近傍の lgroup の空き CPU が割り当てられます。 スレッドのホーム lgroup とは別の lgroup 上の CPU で実行されても、 そのまま維持されるため、 次の CPU 割り当ての際にはホーム lgroup の CPU での実行が優先されます。
ホーム lgroup の CPU を優先的に割り当てるスケジューリングは、 局所性に配慮したメモリ割り当てと並んで、 性能向上における重要な点です。 このようなスケジューリングは、 CPU ノード間のキャッシュ整合性確保コストを低減し、 スレッド切り替えでの立ち上がり時間を短くします。 トランザクション処理のような、 数千スレッドが I/O 待ちで休止しているケースでは、 非常に効果的です。
POSIX 準拠のため、 実時間処理スレッドでは、 上記のようなスケジューリングは実施されません。 MPO による利便性を得るのは、 timeshare (TS), interactive (IA), fixed priority (FX), fair share (FSS:Fair Share Scheduing) に属するジョブのみです。
局所性に配慮したスケジューリングの詳細は、3章も参照してください。
カーネル実装上のスケジュール方針分類定義は:
[16.5/1]@p.xxx 〜 [16.5/3]@p.xxx
Solaris でのメモリ割り当ては、 (1) アプリケーションでの sbrk() によるヒープ拡張や、 ファイルのマッピング契機での仮想アドレス割り当てと、 (2) 当該アドレス領域への最初のアクセス契機での物理メモリ割り当て (物理メモリの選択 ⇒ 仮想空間への貼り付け) の、二段階で構成されます。
sbrk()
メモリの局所性が性能に影響するようなシステムでは、 アクセスするであろうスレッドに「近い」メモリを割り当てることが、 遅延の低減や、帯域の有効利用の上で効果があります。 メモリを割り当て時点では、 アプリケーションのメモリ使用形態を、 カーネルから確実に知ることはできませんが、 幾つかの仮定を元に、 多くの使用形態をカバーすることが可能です。
局所性を考慮した最も単純なメモリ割り当て方針は、 最初に利用したスレッドのホーム lgroup (or その近傍) から割り当てる、 「初回利用」(first touch) です。 この方針は、 最初に利用したスレッドが、当該メモリを一番利用する頻度が高いであろう、 という過程に基づいています。 この方針は主に、 単一スレッドアプリケーションの実行モデルをカバーしますが、 多くの複数スレッドアプリケーションもカバーできます。 プライベートメモリ (ヒープやスタック、private なファイルマッピング) の割り当てでは、 firt touch がデフォルトの方針となります。 スケジューリングにおける CPU 割り当ては、 (先述したように) スレッドのホーム lgroup の CPU が優先されるので、 メモリの割り当て元となる lgroup は、 当該時点でのスレッド実行 lgroup ではなく、 スレッドのホーム lgroup です。
[16.5/4]@p.xxx
その性質上、 共有メモリ (ISM: Intimate Shared Memory や MAP_SHARED によるファイルマッピング等) 領域は、 複数のスレッドから利用されます。 ある程度のスレッドが異なる lgroup 上で実行され得るとの仮定に基づき、 カーネルは共有メモリ領域に「ランダム」に物理メモリを割り当てます。 この割り当て方針は、 「ホスト上のスレッド全体」として平均遅延を最小化しつつ、 メモリ帯域を最適化します。 物理メモリの割り当てをシステム全体に分散させることで、 メモリコントローラやメモリバス帯域に対する負荷が、 特定要素に集中することを防ぎます。 「ランダム」割り当てにより、 実行毎のスレッド/メモリの相対的な局所性が、 大雑把に平準化されるため、 性能測定における再現性も向上します。
「ランダム」であることが、 逆に「計測における再現性の向上 = 平準化」につながる、 というのは興味深い。
[16.6/1]@p.xxx 〜 [16.6/7]@p.xxx
locality group (lgroup) は lgrp 構造体で表現されます。 この構造体には、lgroup 自体の情報や保持する資源に関する、 以下の情報が格納されます。
lgrp
[16.6/8]@p.xxx 〜 [16.6/10]@p.xxx
lgroup プラットフォームハンドルにより、 lgroup に関する実装を、 プラットフォーム非依存な「共通処理」と、 プラットフォーム依存の「固有処理」に分離することができます。 この分離には、 「固有処理」がハードウェア資源の取り扱いに集中できる一方で、 「共通処理」と「固有処理」の間でのきれいな連携や、 カーネル実装の可搬性向上、 「共通処理」をスケジューリング/仮想メモリ/API 処理に集中させる、 といった利点もあります。 プラットフォームハンドルは「固有処理」側で管理するものなので、 CPU やメモリなどのハードウェア資源と lgroup との対応関係を、 任意に実装可能です。
ハードウェアによっては、 実際の遅延算出のために、 CPU やメモリを持たない lgroup の存在が必要になります。 そのため、Solaris の MPO では、 lgroup に対する CPU やメモリの配置、 他の lgroup と関連付けといったものを、 「固有処理」に任せているのです。
データ構造は以下のように定義されています。
「データ構造は以下のように定義されています。」 はコード引用箇所の前にあるべき記述な気が……
「一意の ID」 (lgrp_id_t as lgrp.lgrp_id) は、 Solaris カーネル側で発番した、 プラットフォーム非依存処理向けの識別子で、 「プラットフォームハンドル」 (lgrp_handle_t as lgrp.lgrp_plathand) には、 ハードウェア固有の識別用情報が使用できる。
lgrp_id_t as lgrp.lgrp_id) は、 Solaris カーネル側で発番した、 プラットフォーム非依存処理向けの識別子で、 「プラットフォームハンドル」 (lgrp_handle_t as lgrp.lgrp_plathand) には、 ハードウェア固有の識別用情報が使用できる。
as lgrp.lgrp_id) は、 Solaris カーネル側で発番した、 プラットフォーム非依存処理向けの識別子で、 「プラットフォームハンドル」 (lgrp_handle_t as lgrp.lgrp_plathand) には、 ハードウェア固有の識別用情報が使用できる。
lgrp.lgrp_id
lgrp_handle_t
lgrp.lgrp_plathand
lgrp_id_t ⇒ lgrp_handle_t は必須だが、 逆の変換は必ずしも必要ではないので、 任意の ID 空間管理が可能になる (必ずしもアドレスでなくても良い)。
⇒ lgrp_handle_t は必須だが、 逆の変換は必ずしも必要ではないので、 任意の ID 空間管理が可能になる (必ずしもアドレスでなくても良い)。
[16.6.1/1]@p.xxx 〜 [16.6.1/3]@p.xxx
MPO で導入されている locality 関連の最適化の多くは、 大多数のアプリケーションにおいて適切な性能を得るための、 単純な経験則に基づくものです。 想定と異なる挙動をするアプリケーションは、 適切な性能を得られない可能性があります。 そのような問題ケースでのシステムの挙動は、 MPO 向けの内部変数を元に説明できるかもしれませんし、 次節で紹介する制御用 API によって問題を解決できるかもしれません。
重要: MPO 向けシステム変数の説明は、 単に MPO の実装を説明するためのものです。 変数変更時の挙動は未保証ですし、 障害発生時の適切な診断のためには、 変更された値をデフォルト値に戻す必要があるかもしれません。
これらがカーネル内部変数で、 公式インタフェースではない点に留意してください。 現 Solaris で実装されてる内部変数であっても、 後々(仕様?)変更や廃止の対象になるかもしれません。 それに加えて、 内部変数であることから、 想定外の値が設定された場合のエラー検知等もありません。 各デフォルト値は、お互いが適切に機能するように、 慎重に選ばれたものです。
[16.6.1/4]@p.xxx 〜 [16.6.1/6]@p.xxx
lgrp_mem_default_policy は、 カーネルでのメモリ割り当てで使用するデフォルトのポリシーです。 sys/lgrp.h ヘッダファイルで定義されるポリシーを表す整数値が設定されます。 Sun Fire 3800-6800 (Solaris 9 〜) では、 「初回利用」割り当てポリシーで使用される LGRP_MEM_POLICY_NEXT が設定されます。 Sun Fire 12K/15K では:
lgrp_mem_default_policy
LGRP_MEM_POLICY_NEXT
LGRP_MEM_POLICY_RANDOM
"first touch" に対応するのは LGRP_MEM_POLICY_DEFAULT。
LGRP_MEM_POLICY_DEFAULT
[16.6.1/7]@p.xxx 〜 [16.6.1/8]@p.xxx
lgrp_shm_random_thresh は、 割り当てポリシーに「ランダム」を使用する共有メモリ領域の、 サイズ閾値(下限)です。 デフォルト値の 8MB は、 MPI 併用プログラムでの並列処理 pipe 連携先が使用するような、 連携用バッファを除外するのに十分大きなサイズですが、 メモリアクセスのホットスポットになるような領域を、 システム全体に分散させる値としては、十分小さいです。
lgrp_shm_random_thresh
この内部変数は 64bit 整数値で、 カーネルデバッガによる実行時改変や、 起動時の /etc/system 読み込みで変更可能です。
多数のスレッドからアクセスされる共有メモリは、 「ランダム」割り当ての方がシステム全体としての性能が安定するため、 共有メモリの割り当てポリシーは問答無用で「ランダム」がデフォルトだが、 「並列処理の連携用バッファ」のような特定用途向けには、 「初回利用」の方が効率が良い、 という判断な模様。
[16.6.1/9]@p.xxx
lgrp_mem_pset_aware は、 「ランダム」割り当てポリシーにおいて、 システム中の全 lgroup を対象とするか、 プロセス実行中の processor set 配下の processor が属する lgroup を対象とするかを指定します (後者はプロセスを processor set 配下で実行中の場合のみ)。 デフォルト値は 0 で、 システム中の全 lgroup を割り当て対象とします。 このデフォルト値は、 processor set 機能を使用しないか、 カーネル内部スレッドとの分離にのみ使用するケース向けです。 アプリケーション同士の隔離に processor set 機能を使用する場合は、 この変数を 1 に設定することで、 実行性能の再現性を向上させられます。
lgrp_mem_pset_aware
[16.6.1/10]@p.xxx 〜 [16.6.1/12]@p.xxx
lgrp_expand_proc_thresh は、 複数 lgroup にプロセスのスレッドを拡散させる際の閾値を指定します。 あるプロセスのスレッドが拡散済みの lgroup 間で、 最低 load がこの閾値を超えた場合、 許容上限に到達したとみなし、 新規スレッド生成時には別の lgroup を割り当てます。
lgrp_expand_proc_thresh
lgroup の load 許容率を表す値が設定されます。 カーネル内部処理を整数演算で済ますために、 load 許容率を INT16_MAX 倍した 32bit 符号なし整数を使用します。
INT16_MAX
Sun Fire 12K/15K でのデフォルト値は、 各 lgroup の load 許容率 75% を意味する (INT16_MAX * 3) / 4 です。 Sun Fire 3800-6800 でのデフォルト値は、 load 許容率 25% を意味する INT16_MAX / 4 です。 これらのデフォルト値の違いは、 両者のアーキテクチャの違いに由来します。 別 lgroup での実行に起因する Sun Fire 12K/15K での遅延は、 Sun Fire 3800-6800 よりも顕著ですが、 その分利用可能な帯域が広いです。 そのため、 Sun Fire 12K/15K ではアプリケーションの遅延の抑止を、 Sun Fire 3800-6800 では帯域負荷の分散 (= lgroup 分散による利用均一化) を優先する設定になっています。
(INT16_MAX * 3) / 4
INT16_MAX / 4
プラットフォーム固有の調整が入らない場合 (x64 系含む)、 デフォルト値は LGRP_EXPAND_PROC_THRESH_DEFAULT (= 62250 ≒ 190%) になる模様。 意味合い的には「別 lgroup 実行での遅延が非常に顕著」といったところか。
LGRP_EXPAND_PROC_THRESH_DEFAULT
[16.6.1/13]@p.xxx 〜 [16.6.1/14]@p.xxx
lgrp_privm_random_thresh は、 割り当てポリシーに「ランダム」を使用するプライベートメモリ領域の、 サイズ閾値(下限)です。 デフォルト値は ULONG_MAX (= 「ランダム」使用禁止) です。
lgrp_privm_random_thresh
ULONG_MAX
この内部変数は 64bit 符号なし整数値で、 カーネルデバッガによる実行時改変や、 起動時の /etc/system 読み込みで変更可能です。
プライベートメモリ (ヒープ/スタック等) は、 「初回利用」が効率的だが、 大規模メモリ領域を使用する場合は、 システム全体に分散させた方が性能が安定する、 という判断な模様。
[16.6.1/15]@p.xxx
lgrp_expand_proc_diff は、 スレッドを新規 lgroup に拡散させる際に、 現行 lgroup 群の load に対して、 新規 lgroup の load がどの程度下回っていないといけないかの差分値です。 値は lgrp_expand_proc_thresh と同じ要領で算出します。 Sun Fire 3800-6800 および 12K/15K でのデフォルト値は、 いずれも 25% 差を意味する INT16_MAX / 4 です。
lgrp_expand_proc_diff
プラットフォーム固有の調整が入らない場合 (x64 系含む)、 デフォルト値は (= 60000 ≒ 183%) になる模様。 意味合い的には「余程暇な lgroup でなければ拡散させない」といったところか。
[16.6.1/16]@p.xxx
lgrp_loadavg_tolerance は、 2つの lgroup の load 値の差が、 lgrp_loadavg_tolerance の範囲に収まる場合は、 両者の load は同程度とみなされ、 ランダムに選択された lgroup が新規割り当てに使用されます。 設定値は、先述した load 関連変数と同じ要領で算出されます。 デフォルト値として使用される 0x10000 は、 データベースや各種アプリケーションの混合利用環境で、 良い結果を出しています。 HPC 環境の場合、 0x1000 (≒ 12.5%) のような小さい値の方が適している、 という調査結果も出ています。
lgrp_loadavg_tolerance
原文中では、 デフォルト値として 0x10000 (≒ 200%) が出てくるが、 実際のカーネルコードだと LGRP_LOADAVG_THREAD_MAX (= 0xFFEC = 65516 ≒ 200%)、 あるいはその半分の値が使用されている。
LGRP_LOADAVG_THREAD_MAX
僅かな差だが、なぜキリの良い 0x10000 ではなく、 半端な 0XFFEC なのかは不明。
[16.7/1]@p.xxx
Solaris に新規追加された API によって、 MPO によるアプリケーション性能の最適化手法を、 より深く利用できます。
[16.7.1/1]@p.xxx
個別のアルゴリズムを学ぶだけでは、 メモリ局所性に関連した、潜在的な問題の特定は、 容易ではありません。 逐次処理では発生しなかったメモリ局所性の問題が、 自動並列化コンパイラの利用によって持ち込まれる可能性もあります。 以下で説明するのは、 プロセッサや物理メモリの、 スレッドや仮想メモリ領域への実際の割り当て状況を、 実行時に取得するための API です。 各 API 毎の説明は大まかなものですので、 詳細は Solaris 9 から提供されている man ページを参照してください。
"high-level description" という言い訳^h^h^h言い回しは、 今後積極的に使っていきたいと思いました (笑)
[16.7.1/2]@p.xxx 〜 [16.7.1/3]@p.xxx
getcpuid(3C) は、 呼び出し時点で当該スレッドが実行されている CPU の識別子を返します。 スレッドが CPU に固定されていない限り、 カーネルによって任意に別 CPU へのスケジューリングが行われるので、 呼び出しから復帰した時点での戻り値の妥当性は、 保証されていません。
getcpuid(3C)
lgroup_home(3C) は、 呼び出しスレッドのホーム lgroup の識別子を返します。 別な lgroup 中の CPU に固定されるか、 当該 lgroup 中のすべての CPU がオフラインになるかしない限り、 ホーム lgroup は変更されないので、 CPU よりも変動性の極めて低い値です。 但し、この持続性は、 今後リリースされる Solaris でも維持される保証はありません。 指定された方針に従って、 ホーム lgroup が次々と変更されるようになる可能性もあります。
lgroup_home(3C)
ホーム lgroup の持続性は、 16.3 Initial Thread Placement も参照のこと。
[16.7.1/4]@p.xxx 〜 [16.7.1/5]@p.xxx
meminfo(2) により、 呼び出し元プロセスにおける、 仮想/物理メモリの割り当て状況に関して、 問い合わせができます。 仮想アドレス領域に対しては、 物理アドレス、当該物理メモリが属する lgroup、 および物理アドレスページサイズを返します。 物理アドレスに対しては、 当該物理メモリが属する lgroup を返します。
meminfo(2)
この API の主な用途は、診断と検証になります。 物理メモリ配置を知ることで、 メモリアクセスが想定よりも遅い理由を説明できるかもしれません。 この API で得られる情報を元に、 後述する madvise(3C) を使って、 メモリ配置の最適化に関する情報を、 カーネルに伝えることができるかもしれません。 また、 madvise(3C) 呼び出しにより、 想定通りの最適化ができているか確認する用途でも有用です。
madvise(3C)
[16.7.1.1/1]@p.xxx 〜 [16.7.1.1/2]@p.xxx
MPO におけるカーネル側の目標は、 アプリケーションの変更なしに、 メモリ局所性の問題に対して、 妥当な性能を提供することです。 しかし、 アプリケーションの中には、 カーネルが使用するデフォルトの方針を変更することで、 より良い性能を得るものがあるかもしれません。
巨大なプライベートメモリ領域の確保&初期化を、 あるスレッドが単独で実施する場合、 デフォルトの割り当て方針では、 全てのメモリが単独 lgroup 中に割り当てられます。 初期化の後でメモリ領域にアクセスする大量のスレッドを生成した場合、 多くのスレッドが別 lgroup で実行されることになってしまいます (= 遅延の発生/単一メモリコントローラへの負荷集中)。 このような挙動を変更するために、 アプリケーションへ広範な変更を施すよりは、 madivse(I3C) を使って性能を改善する方が、 相対的に容易です。 madivse(I3C) の利用は簡単ですが、 MADV_ACCESS_* 系フラグの使用は、 オーバーヘッドを伴います。 XXXXXX
madivse(I3C) を使って性能を改善する方が、 相対的に容易です。 madivse(I3C) の利用は簡単ですが、 MADV_ACCESS_* 系フラグの使用は、 オーバーヘッドを伴います。 XXXXXX
を使って性能を改善する方が、 相対的に容易です。 madivse(I3C) の利用は簡単ですが、 MADV_ACCESS_* 系フラグの使用は、 オーバーヘッドを伴います。 XXXXXX
madivse(I3C) の利用は簡単ですが、 MADV_ACCESS_* 系フラグの使用は、 オーバーヘッドを伴います。 XXXXXX
の利用は簡単ですが、 MADV_ACCESS_* 系フラグの使用は、 オーバーヘッドを伴います。 XXXXXX
MADV_ACCESS_*
"16.7 MPO APIs" 配下で、 「情報採取系」に続き「設定変更系」の説明を行っているのだから、 章立て的には、 この節は 16.7.2 として、 "16.7.1 Informational" と同じレベルの方が良いような気が……
[16.7.1.1/3]@p.xxx 〜 [16.7.1.1/5]@p.xxx
madvise(3C) により、 指定範囲のメモリ領域に対してアプリケーションが想定しているアクセス形式を、 カーネルに対して通知できます。 具体的には 特定領域を多数のスレッドがアクセスする (MADV_ACCESS_MANY) のか、 二番目にアクセスしたスレッドのみが継続的にアクセスする (MADV_ACCESS_LWP) のかの指定になります。
MADV_ACCESS_MANY
MADV_ACCESS_LWP
MADV_ACCESS_MANY が想定しているのは、 単一スレッドがプライベートメモリ領域中を確保/初期化した上で、 領域全般にアクセスするスレッドを大量に生成するアプリケーションです。 これは自動並列化を用いた多くのアプリケーションで共通の挙動です。 単一スレッドによるメモリ領域の確保/初期化に対して、 メモリを単独 lgroup 中に割り当てるのがデフォルトの割り当て方針ですが、 MADV_ACCESS_MANY 通知により、 特定のメモリコントローラにアクセスが偏らない (= 分散による帯域の最適化) ように、 システム中の lgroup 全体に分散したメモリ割り当てを行います。
MADV_ACCESS_LWP が想定しているのは、 特定範囲へのアクセススレッドが逐次変化するようなケースです。 指定範囲にアクセスしたスレッドの lgroup が、 メモリの属する lgroup と異なる場合、 カーネルはメモリをスレッドの属する lgroup に移動させます。 特定メモリ範囲にアクセスするスレッドが、 順次入れ替わるような「多相」アプリケーション以外に、 ラージページでのメモリ確保のために、 巨大な ISM セグメントを割り当てつつも、 スレッド間でセグメント全体を共有するわけではない場合でも有用です。 但し、 MADV_ACCESS_LWP におけるメモリ移動は時間消費を伴います。
MADV_ACCESS_LWP は、 「三番目以降のスレッド」に対しても、 lgroup 間移動を継続的に行うのかな?
「一定回数 (or 頻度?) 以上のアクセスの後に移動」 みたいな形式じゃないと、 ちょっとした覗き見だけで、 無用なピンポンが発生するような気が……
[16.7.1.1/6]@p.xxx
madv.so.1 は、 アプリケーションへの改変無しで、 メモリ割り当て実行の際に先述したようなヒント情報を差し込むための、 共有ライブラリです。 アドレス範囲の指定等ができないので、 指定対象は「ヒープ全体」「ISM/DISM 領域全体」「プライベートメモリ全体」等々、 madvise(3C) よりも精密さには欠けますが、 簡易的な確認や、 ソースコードを入手できない場合には有用です。
madv.so.1
[16.7.1.2/1]@p.xxx 〜 [16.7.1.2/4]@p.xxx
lgroup API により抽象化された lgroup 情報を取得できます。 liblgrp ライブラリにより提供される lgroup API には、 以下のような機能があります。
[16.7.2/1]@p.xxx 〜 [16.7.2/2]@p.xxx
lgroup API を使用する前に、 利用可能な機能を確認する lgrp_version(3LGRP) を使用する必要があります。
lgrp_version(3LGRP)
lgrp_version(3LGRP) は lgroup インタフェースバージョン番号を引数に取り、 システムがサポートしているバージョン番号を返します。 引数で指定されたインタフェースがサポートされている場合、 lgrp_version(3LGRP) は引数で指定されたバージョン番号を返します。 それ以外の場合は、 LGRP_VER_NONE を返します。
LGRP_VER_NONE
[16.7.3/1]@p.xxx 〜 [16.7.3/4]@p.xxx
lgroup 階層の走査や、階層中の lgroup の情報を行う場合、 それらに先立って lgrp_init(3LGRP) を実行する必要があります。 この関数は、 当該時点における一貫性の取れた lgroup 階層のスナップショットを提供します。 スナップショットに含まれる資源として、 「呼び出しスレッドから利用可能なもの限定」 (LGRP_VIEW_CALLE) と 「OS で利用可能なもの全て」 (LGRP_VIEW_OS) を選択可能です。 戻り値として、以下の処理で使用する「クッキー」が返却されます。
lgrp_init(3LGRP)
LGRP_VIEW_CALLE
LGRP_VIEW_OS
lgroup 構成は動的に変更可能なので、 「(現時点における) 有効性」は確かに重要かも。
NFS の READDIR なんかも、 走査中の改変検知用に cookie を保持するようになっている。
[16.7.3/5]@p.xxx 〜 [16.7.3/6]@p.xxx
lgrp_init() は lgroup インタフェースの初期化と、 lgroup 階層スナップショットの作成を行います。
lgrp_init()
lgrp_fini() は、 指定された cookie に対応する lgroup 階層スナップショットを破棄します。
lgrp_fini()
以下未稿
統計情報の話なので割愛
MDB に関する説明なので割愛