Java

Threading

See also Performance Docs

はじめに

この文書は、 SolarisTM オペレーティング環境のスレッディングモデルと JavaTM のスレッドモデルの間の関係について概要を説明するものです。 Solarisのスレッディングモデルについての設定によって、 Solarisオペレーティング環境でのJava実行環境 (JRE) の性能は大きく変わり得ます。

Java言語はマルチスレッドを生来の機能として持つため、 OSが持つスレッドの実装がアプリケーションの性能に大きな影響を 与えることがあります。 幸運にも (または不幸にも)、複数のスレッディングモデルを選択でき、 また、各モデルにおいても異なる同期方法を選択できます。 何を選択できるかは、VMによって様々です。 混乱を増長するかのように、Solaris 8から9にかけて スレッドライブラリが移行する予定です。 これによって、多くの選択肢がなくなります。

バージョン1.1はgreen threadsベースであり、ここでは扱いません。 green threadsはVM中でシミュレートされたスレッドであり、 1.2にてOS本来のスレッディングモデルに移行する以前に使われていました。 green threadsは、Linuxにおいては、あるひとつの利点があったのかもしれません (ネイティブスレッドひとつについてひとつのプロセスを起動する必要がない)。 しかし、1.1からVM技術が大きく進歩し、 過去数年の性能向上によって、以前green threadsが持っていた利点はなくなりました。


Solarisのスレッディングモデル

Solarisでは、2つのスレッディングモデルを利用できます。 すなわち、多対多 (many-to-many) モデルと一対一 (one-to-one) モデルです。 多対多と一対一は (本質的には) LWPs (lightweight processes) と Solarisスレッドに関係しています。 LWPはカーネルスレッドを持ちます。 LWPの上にあるものは、カーネルがCPUに割り当てます。 スレッド上にあるものは、CPUに割り当てられる前に、 スレッドライブラリがLWPに割り当てる必要があります。 後者の方式のどこに利点があるのでしょうか? LWPがたくさんあると多くの状態およびカーネル内資源が必要となるので、 LWPの数を少なく保つことでカーネルを軽く、すばやい状態に保て、 性能を向上させられるのです。 この方式がどうして問題になるのでしょうか? 一定時間スレッドがLWP上に割り当てられないことで、飢餓 (starvation) が起こることがあるのです。

次の図を見て下さい:

Javaスレッドは実際はSolarisスレッドです。 これは、1.2のVMからはOS本来のスレッディングモデルを使っているためです。 左側の図には多対多モデルが描かれています。 このモデルでは、Solarisスレッドは、Solaris libthread.soライブラリによって LWPへの割り当てが行われます。 LWPはカーネルスレッドと一対一で対応しています。 右側の図は一対一モデルです。 このモデルでは、SolarisスレッドはLWPと結びつけられています。 このモデルでは (スレッドごとにLWPが要るため) より多くのLWPが作られることになります。 このことの影響は後ほど調べます。

多対多モデルは、Solaris 9より前のSolarisでは既定のモデルです。 Solaris 8には一対一モデルのための "alternate" (代わりの) スレッディングライブラリがあります。 しかしSolaris 7以前では多対多モデルしか使えません。 (または、bound threadsでそれを模するか。) 混乱を増長するだけかもしれませんが補足しておくと、 Solarisスレッドを作成する際、スレッドをずっと特定のLWPに結びつけておく bound threadsという指定ができます。 これによって、事実上、一対一モデルにすることができますが、 後述するオーバーヘッドが伴います。


同期

多対多モデルでは、HotSpotには同期 (synchronization) の方法が 2種類あります。LWPベースの方法とスレッドベースの方法です。 Solarisの文書で言うところの、USYNC_PROCESS (LWPベース) と USYNC_THREAD (スレッドベース) に相当します。 手もとのSolarisの/usr/include/sys/synch.hを見てください。 LWPベースの同期は、プロセス間でも動作する必要があるため、 重いものと考えられています。 それに対して、スレッドベースの同期はプロセス内で行えます。 現在のところ、複数のJava仮想マシンには 大域的なデータを共有する機能はありませんし、 アプリケーションにとっては明らかにスレッドベースの同期だけあれば充分です。 しかし、J2SE 1.3以降では両方の方法を使えます。 なぜかはすぐに説明します。


組み合わせての調査

さて、選択肢を振り返ってみましょう。 J2SEのすべての版、Solaris OSのすべての版で、 上述の選択肢のすべてを選べるわけではないことを心に留めておいてください。

機能Solaris 8より前Solaris 8Solaris 9
多対多, スレッドベースの同期1.3*,1.41.3*,1.4使えません
多対多, LWPベースの同期1.2*,1.3,1.4*1.2*,1.3,1.4*使えません
一対一, bound threads1.3,1.41.3,1.4使えません
一対一, alternateスレッドライブラリ使えません 1.2,1.3,1.41.2*,1.3*,1.4*

*:注: このVMの既定値

表を見ると、ある版のSolaris OSは、 VMが持っている機能を使えないということが判ります。 例えば、VMが一対一モデルをalternateスレッドライブラリで使えるにもかかわらず、 その機能はSolaris 7にはありません。 また、Solaris 8のalternateスレッドライブラリが Solaris 9では唯一のスレッドライブラリになるということが判るでしょう。 これはつまり、多対対モデルが公式に引退するということを意味します。 J2SEの1.2、1.3、1.4で利用可能な多対多モデルを、 Solaris 9では利用できないということです。

Solaris 9では何か問題があるでしょうか。 スレッディングモデルの選択肢を減らすことは、 物事をわずかばかり単純にします。 性能ははるかに優秀です。 多対多モデルに合うように過度に調整されたコードの性能は低下しますが、 多くのコードの性能はかなり向上します。 alternateスレッドライブラリを試してください。 現在、Solaris 8にも含まれています。 alternateスレッドライブラリを使うために 既存のコードを再コンパイルする必要はありません。 インタフェースはそれ以前のスレッドライブラリと同じなので、 Solaris 8で使うためには単に LD_LIBRARY_PATHに/usr/lib/lwpを含めるだけで済みます。

どうやってスレッディングモデルを選ぶのか?

ここまでで、スレッドの様々なモデルと同期の技術についてすべて述べました。 実際に試す時が来ました。 あなたのアプリケーションが少数のスレッドしか使わないのであれば、 大きな違いは見られないでしょう。 例外は、Solaris 8やそれ以前の上で、1.3と1.4を既定の設定で使ったときに 飢餓が起こり得るということです。 この表が対象とするのはSolaris 9より前の版だということに注意してください。

機能 1.2のオプション 1.3のオプション 1.4のオプション
多対多,
スレッドベースの同期
使えません 既定値 -XX:-UseLWPSynchronization
多対多,
LWPベースの同期
既定値 -XX:+UseLWPSynchronization既定値
一対一,
bound threads
使えません -XX:+UseBoundThreads-XX:+UseBoundThreads
一対一,
alternateスレッドライブラリ*
export LD_LIBRARY_PATH=/usr/lib/lwpexport LD_LIBRARY_PATH=/usr/lib/lwpexport LD_LIBRARY_PATH=/usr/lib/lwp
*注: Solaris 9では何も指定せずとも alternateスレッドライブラリが使われるので、 LD_LIBRARY_PATHに/usr/lib/lwpを加えないでください。


Java Performance Group内で得た知見

1.3の既定のモデルについて、2つの問題を発見しました。 とはいえ、1.3にはこれらの問題を回避するためのオプションがあります。

一般には、多対多モデルとスレッドベース同期の組み合わせが優秀です。 ただし、稀に、ほどほどのスレッド数 (約CPU数) で飢餓が起きました。 ある実験では、スレッド数をCPU数の2倍までゆっくりと増やし、 各スレッドには同じだけの仕事を割り当てました。 そして、最も多くの仕事をしたスレッドと少しの仕事しかしなかったスレッドの差を 測ったところ、Solaris 8の"alternate"スレッドライブラリを使うことで その差が29%から8%まで減りました。 その反面、性能にはそれほど大きな影響はありませんでした (1-2%)。 我々は、この実験から、 スレッドモデルが一般に性能には大きな影響を与えないことと、 飢餓が起きた場合には別のモデルを試すことに価値があることを学びました。 補足すると、1.2では特に指定しない限りはLWPベースの同期が使われるので、 飢餓の問題は起きません。

もうひとつの問題はスケーラビリティに関するものです。 それは別の形の飢餓であり、我々は、多数のスレッドを扱うために充分なだけの LWPが作られないことに気がつきました。 30 CPUのマシンで2000のスレッドを走らせる実験で、 SolarisスレッドとLWPの数の比がおよそ2:1となっていて、 このことが計算中心のアプリケーションのスループットを 大きく制限してしまっていることに気づきました。 LWPは通常、スレッドがカーネル内でブロックした時に作られます。 しかし、アプリケーションがブロックせずに計算を続けるだけの場合には、 性能の低下が見られることがあります。
-XX:+UseLWPSynchronizationを使うとこの比は1:1になり、 各Solarisスレッドに対してひとつのLWPが作られます。 とはいえ、この場合、それらのスレッドはLWPに結びつけられるわけではありません (各スレッドはLWPからLWPへと渡り歩くかもしれません)。 これによって、スループットは7倍になりました。 bound threadsを指定して一対一モデルとすることでも LWPとSolarisスレッドの数の比は1:1となるので、 あなたは、LWPベースの同期と同じ効果が得られると予想するかもしれません。 しかし、この場合、性能は (最悪で) 80%以上低下しました。 これは予期していなかったことです。 SolarisスレッドをLWPに結びつけることに、 何らかの大きなオーバーヘッドがあるに違いありません。 最後に、(Solaris 9でも動作する) Solaris 8の"alternate"スレッドライブラリ を用いて一対一モデルを実験したところ、 最良の性能が得られました。 LWPベースの同期と比較しても15%以上の向上であり、 スレッドベースの同期を用いたオプション指定なしの実行方法と比較すると 8倍近い性能となっています。 この結果は典型的だとは言えませんが、 スレッドを多用するアプリケーションが非常に敏感であることを示しています。

様々なSolarisマシンでの実験結果を示します。 すべて、Solaris 8でJVM 1.3.1を用いた場合の結果です:
アーキテクチャCPU数スレッド数モデル スループットの違い (オプション指定なしの結果に対する比)
Sparc30400/2000オプション指定なし ---
Sparc30400/2000LWPベースの同期 215%/800%
Sparc30400/2000bound threads -10%/-80%
Sparc30400/2000alternate 一対一275%/900%
Sparc4400/2000オプション指定なし ---
Sparc4400/2000LWPベースの同期 30%/60%
Sparc4400/2000bound threads -5%/-45%
Sparc4400/2000alternate 一対一30%/50%
Sparc2400/2000オプション指定なし ---
Sparc2400/2000LWPベースの同期 0%/25%
Sparc2400/2000bound threads -30%/-40%
Sparc2400/2000alternate 一対一-10%/0%
Intel4400/2000オプション指定なし ---
Intel4400/2000LWPベースの同期 25%/60%
Intel4400/2000bound threads 0%/-10%
Intel4400/2000alternate 一対一20%/60%
Intel2400/2000オプション指定なし ---
Intel2400/2000LWPベースの同期 15%/45%
Intel2400/2000bound threads -10%/-15%
Intel2400/2000alternate 一対一15%/35%

ご覧のように、2 CPU、4 CPUでの結果は、 30 CPUでの結果とはまた大きく異なっています。 2 CPUではLWPベース同期の結果が最良であり、 4 PCUでは"alternate"スレッドライブラリの結果が LWPベース同期の結果と同じでした。 bound threadsを指定した場合は、30 CPUと同様に、 向上しないか、または、大きくスループットが低下しました。 2 CPUでスレッド数を400に減らすと、LWPベース同期の結果は オプションを指定しない場合の結果と同じになり、 bound threadsのコストは30%、 alternateスレッドライブラリのコストは10%となりました。 IntelのCPUを搭載した4 CPUのSolarisマシンでも、 SPARCと同じような結果が得られました。 しかし、bound threadsの性能はSPARCより良く、 性能低下はごくわずかかまったくないか、でした。

また、スレッドベースの同期を用いる既定のモデルを避けることで、 実験結果の予測可能性が向上することを経験しました。 他のモデルを用いることで、飢餓によって生じる結果のばらつきが なくなったのだと思われます。


スレッド数が多い場合に考慮すべき他の事項


多数のスレッドを扱う際に考慮することは、スレッディングモデルの他にもあります。 すなわち:

特に指定しない場合の既定のスタックサイズはかなり大きいです: 1.3と1.4の32 bit VMの場合、SPARCでは512 KB、Intelでは256 KBです。 また、1.4のSPARC用64 bit VMの場合は1 MB、1.2のVMでは128 KBとなります。 多数 (数千) のスレッドがある場合、スタック空間をかなり無駄に使ってしまいます。 最小のスタックサイズは1.3と1.4での64 KB、1.2では32 KBです。 これは-Xssフラグを使うことで設定できます。

TLE (1.3) や TLAB (1.4) は、young generation用ヒープ中の スレッドごとに割り当てられた領域です (HotSpot Garbage collection Tuning Document参照)。 これらを用いることで、スレッド数が少ない場合 (数百) には大きく性能が向上します。 しかし、スレッド数を増やしていくと、スレッドごとのヒープが ヒープ全体のうちかなりの割合を消費してしまいます。 これによって、ガーベジコレクションがより頻繁に起きてしまいます。 1.3では-XX:-UseTLEを、1.4では-XX:-UseTLABフラグを与えることで、 スレッドごとのヒープをまったく確保しないようにできます。 また、その代わりに、スレッドごとのヒープのサイズを指定することもできます。 そのためには1.3では-XX:TLESize=<value>、 1.4では-XX:TLABSize=<value>フラグを与えます。 オプションで指定せずともTLEやTLABが有効になるのは、 SPARC用の-server JVMのみだということに注意してください。

同様に、ガーベジコレクションも性能に非常に大きな影響を与えることがあります。 この文書を見てください: document on tuning garbage collection

ISM (Intimate Shared Memory) もまた、メモリを酷使するアプリケーションの性能を 向上させるために利用できます。 これは非常に特殊なオプションで、この機能を使うためには OSのパラメータをいくつか設定する必要があります。 この機能を用いることで、10%かそれ以上の性能向上が得られる可能性があります。 詳細は、この文書を見てください: Big Heaps and Intimate Shared Memory

まとめ

Solarisの別のスレッディングモデルを用いることで、 アプリケーションの性能が変わることがあります。 1.3と1.4のVMにはスレッドに関する無数のオプションがあり、 アプリケーションに応じた最良のオプションを選ぶことができます。 特にオプションを与えない場合の1.3の既定のモデルは一般には優秀なのですが、 スレッドやCPUの数が多い場合には最良の選択ではありません。 アプリケーションが1つ以上のスレッドを用いるならば、 様々なスレッディングモデルを試すことをおすすめします。 また、多数のスレッドやCPUでもアプリケーションをうまく動作させたければ、 性能に影響を与え得る他の要因に対しても配慮することです。