技術評論社 JAVA PRESS Vol19より.
JavaVMやバイトコード自体はJava言語とは独立したものですが,これから 説明するバイトコードは,メソッド呼び出しとリターン,synchronized文,例 外処理など,いずれもJava言語を強く意識した設計になっています.バイトコー ドの中でも最もJava言語と深く関わっている部分と言えるでしょう.
これらは,Java言語にもっとも近い部分だけに全般に抽象度が高く,粒度 も大きい命令です.「一つの変数に書き込む」というようなミクロな,実装に 近いレベルのものではなく,「メソッドを呼び出す」というようなマクロな, 実装から遠いレベルで定義されています.
一般的に言って複雑なバイトコードほど実装を知らないとイメージは掴み にくいものですが,その中でも今回扱う命令は特にその傾向が強くなって います.また処理効率やメモリ消費など,何を優先するかによって実装方法も 様々で,これも理解を困難にしている原因の一つです.今回紹介する実装法 はごく単純なもので参考程度のものですが,バイトコードを理解する助けには なるでしょう.
メソッド呼び出し命令とリターン命令では,フレーム間に跨って処理が 行われます.このため,これまでのようにカレントフレームとインスタンス という視点で見ている限り,その動作を理解することはできないでしょう.
今までにも何度か触れていると思いますが,JavaVMには4種類のメソッド 呼び出し命令があります(表1).これらの命令ではメソッド呼び出しを 行うという点では共通ですが,呼び出すメソッドの種類が異ります.その 結果,オペランドやオペランドスタックの使い方,ベリファイすべき項目, 実装レベルでのメソッド検索方法やデータ構造なども,それぞれ異ります.
ニモニック | オペランド | 説明 |
---|---|---|
invokevirtual | u2 index | non-staticメソッド(スーパークラスメソッド,private, |
invokespecial | u2 index | non-staticメソッドのうち,スーパークラスメソッド,privateメソッド,インスタンス 初期化メソッド(コンストラクタ)を呼び出す. |
invokestatic | u2 index | staticメソッドを呼び出す |
invokeinterface | u2 index,u1 nargs,u1 0 | インターフェースメソッド(注1)を呼び出す |
(注1:この部分で少し用語が混乱してますが,要するにインターフェース メソッド参照を通して呼び出されるnon-staticメソッドのことです.多重継承 のためにメソッドの検索方法が全く異るものの,実体としてはnon-staticメソッ ドになります.この辺はJava言語と同じです.)
indexはいずれもコンスタントプールへのインデックスで,該当するエント リはCONSTANT_Methodref_info(invokeinterfaceのみ CONSTANT_InterfaceMethodref_info) でなければならず,決定処理が必要にな ります.実際に呼び出すべきメソッド(注2)が何でなければならないかは,そ れぞれ異ります.例えばinvokestatic では実際に呼び出されるメソッドは staticメソッドでなければなりませんし,それ以外のメソッドが呼び出された ならばエラーになります.他の命令でも同様です.
(注2:JavaVMから見ると,メソッド参照が呼び出すべきメソッドの実体が 本当に決定されるのはコンスタントプールの決定の時になります.それ以前は 実体が何になるか,或はそもそもそのようなメソッドが実在するのかすら分か りません.)
いずれの命令においても,メソッドの検索,新しいフレームの生成,古い フレームに関する情報やPCの保存,引数の設定,モニタの獲得(注3),メソッ ドの起動などの処理が行われますが,詳細は異ります.
(注3:正確には獲得又は再入する(is acquired or reentered.).モニタは 単なるロックとは異り,複数回獲得(再入)することが可能でそのたびにカウン タがインクリメントされる.逆に解放しようとする場合も複数回退去(exited) して,カウンタが初期状態まで戻った時に初めて本当にモニタが解放 (released)される.)
文字通りstaticメソッドを呼び出す(つまりinvokeする)命令です.
図1(a)にカレントフレームの視点で見たinvokestaticの例を示します.オ ペランドスタック上に引数がプッシュされており,メソッド呼び出し後,それ らの引数は新しいカレントフレームのローカル変数へと移動します.なお,こ れらはあくまでカレントフレームの視点で見たものに過ぎないので注意してく ださい.
invokevirtualはprivate,スーパークラスメソッド,インスタンス初期化 メソッド"<init>"を除く,通常のnon-staticメソッドを呼び 出す命令です.Java言語においては,最も標準的なメソッド呼び出しと言って 良いでしょう.
図1(b)にinvokevirtualの例を示します.このレベルでのinvokestatic と の一番の違いは,引数の先頭に呼ばれた側にとっての"this"ポイン タが追加されている点です.このため同じメソッドディスクリプタであっても, invokestaticとinvokevirtualでは,オペランドスタック上の引数の個数が異 ります.
さらに,invokevirtualとinvokestaticでは呼び出すメソッドの種類が異る ため,実装レベルではメソッドの検索方法が全く別の物になることが多いでしょ う.前回紹介した実装例では,invokestaticではアドレス(ポインタ)を使って 直接メソッドを呼び出すのに対し,invokevirtualやinvokespecialではメソッ ドインデックスとメソッドテーブルを使って呼び出します(注4).
(注4:これはあくまで実装例ですが,メソッドテーブルは基本的なテクニッ クであり,多くの実装で使われていると思います.なおメソッドテーブル自体 はJavaVM仕様書でも触れられています.仕様書の初版ではメソッド呼出し命令 の定義でも使われていましたが,このような実装依存の記述は仕様書のミスで す.そのため第二版ではメソッドテーブルに触れない形に記述が変更されてい ます.)
もし,呼び出したメソッドがsynchronizedメソッド(注5)だった場合は, メソッド呼び出し命令はモニタの取得も行います.
(注5:synchronizedメソッドかどうかの判別は,method_infoの access_flags のACC_SYNCHRONIZEDフラグで行います.このため, access_flagsの情報は実行時にも必要ですが,その情報をどのような形式で保 存するかは実装依存になります.)
non-staticメソッドのうち,スーパークラスメソッド,privateメソッド, インスタンス初期化メソッド(注6)を呼び出すための命令です.オペランド スタックの使い方はinvokevirtualと同じです.
(注6:既に何度も触れている通り,コンストラクタに相当する部分のクラ スファイル中でのメソッド名は特殊な名前<init>になっています.また クラスの初期化メソッド<clinit>は,いかなるメソッド呼び出し命令で も呼び出すことはできません.<clinit>はJavaVMが内部的に呼び出すの みです.なお,通常は(もしあれば)non-staticフィールドの初期化の処理も <init>で行われるので,厳密にはJavaのソースのコンストラクタと <init> とは同じ物ではありません.この辺はJavaVM仕様書でもあまり 区別されてませんし,そもそもコンパイルはJavaVMの仕様とは関係ないので, どうでもいい部分ではあります.)
どうもこの命令の仕様については少々理解に苦しむ所があるのですが,要 は文字通り特殊なメソッド用の命令ということのようです.ベリファイヤやメ ソッド呼び出しから見ると,これらのメソッドは通常のメソッド (invokevirtualで呼び出されるメソッド)とは少し異る処理が必要になります.
図2(a)が,前回紹介したinvokevirtualのメソッド呼び出しの実装例です. ここでのポイントは,コンパイル時の参照型のクラスではなく実行時の インスタンスのクラスに対応したメソッドテーブルが使用されるという 点でした.
図2(b)にinvokespecialでprivateメソッドを呼び出した例を示します. その場合.当然呼び出されるメソッドはカレントクラスのメソッドである ことがベリファイでチェックされます.
invokevirtualでは実行時に動的に呼び出すメソッドを検索しますが, privateメソッドではコンパイル時に静的に決まっているので,実行時に検索 する必要はありません.コンスタントプールの決定でstaticメソッドと同様に メソッドを指すポインタにしておけば,直接呼び出すことが可能です.ただし invokestaticとは引数が異ります.(thisポインタがある.)
静的に決定されるという性質は<init>でも同じです.(<init> は継承されないため.)なお<init>は特殊なメソッドであり,インスタ ンス生成後の最初の分岐前に(ifne,goto等の前に)必ず一度だけ実行する必要 があり,これもベリファイでチェックされます.
図2(c)にスーパークラスメソッドの例を示します.この場合は参照型が 差しているインスタンスの,その直接のスーパークラスのクラステーブル を使ってメソッド呼び出しを行います.こちらはinvokevirtualと同様に, 実行時に動的にメソッドを検索する必要があります.
文字通りインターフェースメソッドを呼び出す命令です.オペランドスタッ クの使い方だけならばinvokevirtualと同じですが,メソッドの検索方法が全 く異ります.以前少し触れましたが,インターフェースは多重継承が許されて いるために,単一継承であるinvokevirtualで使われるような単純なメソッド テーブルでは実現できません.正直言ってインターフェースメソッドの効率的 な実装までは手が回らないので,これ以上の説明は省略します.
なおinvokeinterfaceのオペランドに無駄な「0」が含まれているのは, quick 疑似命令(注7)に書き換える際に必要となる領域を確保するためだと考 えられます.
(注7:quick疑似命令はSUNの実装依存のもの.コンスタントプールの決定 と同時にバイトコードを書き換えることで,若干効率を上げることができる.)
リターン命令は文字通りメソッドからのリターンを行う命令です.ireturn, areturnなど戻り値の型ごとに別の命令が用意されている他,戻り値のない void用のreturn命令があります(表2).なお,戻り値の型はメソッドのリター ンディスクリプタ(注8)と同じでなければなりません(注9).このことはベリファ イで静的に検証されます.
(注8:戻り値の型を表す文字の並びで,メソッドディスクリプタ中に含ま れています.メソッドディスクリプタはそのメソッドのmethod_info の descriptor_indexが指すコンスタントプールのエントリに,Utf8形式で保持さ れています.)
(注9:つまり,メソッド宣言で示された戻り値の型と,実際にリターン命 令で返される型は同じでなければならないというだけのことです.なお,正確 には参照の場合は代入互換までは許されます.代入互換とは平たく言うとキャ スト可能ということです.)
ニモニック | 引数 | 説明 |
---|---|---|
return | なし | メソッドからリターンする.戻り値はない. |
ireturn | なし | メソッドからリターンする.オペランドスタックの 先頭が戻り値になる.戻り値はint型でなければならない. |
※参照型,float型,long型,double型用の命令は, ireturnのiの部分がそれぞれ,a,f,l,dになる他は同じ. |
メソッドがsynchronizedメソッドの場合は,メソッド呼び出しとは反対に モニタの解放(注10)を行います.
(注10:正確には解放又は退去(released or exited).)
図1(c)にカレントフレームから見たireturnの動作を示します.この例では オペランドスタックのトップにある戻り値を,旧フレームのオペランドスタッ クへプッシュしています.メソッド呼び出し命令の時と同様,これもあまり役 に立つものではありません.
Javaでは排他制御機構としてモニタ(注11)が採用されていますが,Java には(Java言語でもJavaVMでも)次のような二種類のモニタがあります.
(注11:排他制御機構の一種.排他制御機構としては他にもロック,セマフォ などがある.モニタを実装には,OSが用意している単純なロックを使って実装 するか,アセンブラレベルで記述することになるだろう.なお排他制御の実現 にはハードウエアによる支援が不可欠で,そのために通常のプロセッサには Test-and-Set等の命令が用意されている.)
メソッド呼び出し命令で取得するモニタもこの二種類で,staticメソッド 用のinvokestaticでは2のクラスに対応したモニタを,それ以外では1のインス タンスに対応したモニタを,それぞれ獲得します.もちろん獲得出来なかった 場合は待ち状態になるわけです.
モニタの実体は実装依存ですが,最低でも次のようなものが必要になるで しょう.
モニタの実体をどこに置くかも実装依存です.参照に対応したモニタは, ほとんどの場合はインスタンスの実体のヘッダ部に置くことになると思い ますが(注12),クラスに関する構造体は実装依存の部分が多いためクラスに 対応したモニタを置く場所は一概には言えません.
(注12:初期のものだけかも知れませんが,HotSpotVMの実装では各インス タンスに保持するのは2bitのみで,それ以外の情報は必要に応じてJavaスタッ ク上に保持する方法が取られていたようです.)
モニタは各インスタンス毎に一つずつ必要なために消費メモリの点でも重 要なだけでなく,マルチスレッドプログラミングのボトルネックとなることも 多いので,実装には細心の注意を払う必要があるでしょう.
次にメソッド呼び出しとリターンについて,もう少し詳しく見てみましょう. 図3(a)に実装モデル通りのinvokevirtual〜areturnの動作を図示します.
これらの命令では,メソッドを呼び出すことも重要ですが,それに関連して 新しいJavaフレームの生成,引数のセット,さらにはメソッドがsynchronized メソッドの場合はモニタの取得も行われます.これらの処理のほとんどが実装 依存となります.
まず最初にメソッドの引数をオペランドスタック上にプッシュ(図3(a2)). プッシュする方法は定められていませんが,通常はロード命令やフィールド アクセス命令などを使って行います.
もちろん,引数はメソッドディスクリプタとの整合性が保たれなければな りません.例えばこの例では,ディスクリプタが(IF)Ljava/lang/String; な ので,オペランドスタックの先頭三つは参照型(thisポインタになる),int型, float型の変数でなければなりません.
また,メソッドはnon-staticメソッドでなければなりません.これらは通 常はロード時にベリファイヤが一度だけチェックします.
non-staticメソッドではメソッドディスクリプタに書かれているもの以外 に,引数として"this"ポインタが必要となります.staticメソッド ではthisポインタが不要なため,同じメソッドディスクリプタでも引数が一つ 少なくなります.
もしこの段階でメソッド参照が未決定の時は,バイトコードの実行に先だっ てメソッド参照の決定処理(注13)を行います.
(注13:invokestaticの場合は,この過程でクラスのロード,リンク,初期 化などが起ることもあります.invokevirtualの場合は,この命令の実行に先 立ちインスタンス生成が行われているはずで,その段階でクラスのロード等は 行われていなければなりません.)
引数リスト中の"this"ポインタがnull参照でないかチェックし ます.もしnullならばNullPointerExceptionを発生させ,例外処理に移ります.
通常はメソッドテーブルを使って検索するでしょう.該当するメソッドが 存在することはリンク時にチェックしていますので,メソッド呼び出しの段階 では必ず見つかることが保証されています.
もしそのメソッドがsynchronizedメソッドならば,クラスに対応したモニ タの取得を行います.取得出来た場合は次に進みますが,出来なかった場合は 待ち状態に入ります.
このフレームが新たなカレントフレームになります.
なお,実装レベルでのフレームの生成は,フレームを確保できるだけの領 域がJavaスタック上にあるか確認した後,幾つかのポインタを設定したり現在 の実行状態を保存する程度になるでしょう.フレームのサイズはコンパイル時 に静的に決まっており,クラスファイル中のCode属性のmax_stackと max_localsより計算します.
スタックがオーバーフローした場合の処理は実装依存です. StackOverFlowErrorを出して終了することもあるでしょうし,新たなJavaスタッ ク領域を確保することも考えられます(注14).
(注14:Javaスタックは必ずしもメモリ上で連続している必要はありません.)
実装レベルでは他にも様々な情報をフレームにセットすることになります. 例えば,いままでのフレームの各種情報,PC,コンスタントプール,カレント メソッドの実体へのポインタなどが考えられます.この辺りは実装に強く依存 する部分でもあります.
メソッド本体のバイトコード部の先頭のアドレスをPCに設定して, 実行を開始する.
リターン命令が呼び出された時点で,オペランドスタックの トップには戻り値がセットされている必要があります(図3(a4)). これはベリファイ時に静的にチェックされます.
もし現在のメソッドがsynchronizedメソッドならば,メソッドの種類 (static/non-static)に合わせて適切なモニタを解放します.
現在のフレームのオペランドスタックのトップを 一つ前のフレームのオペ ランドスタックにプッシュします.
現在のカレントフレームを破棄し,一つ前のフレームを新たにカレントフ レームとします(図3(a5)).
実装レベルでは,この時に現在のフレーム情報を破棄し,古いフレームに 関する情報などを復元する程度になります.
呼び出し元の,メソッド呼び出し命令の次の命令のアドレスをPCに セットし,メソッドの実行を再開します.
Javaフレームは,モデルでは図3(a)のようになりますが,実装レベルでは 配列を使うことになるでしょう.図3(a)の例を,配列を使って実装した 場合の例を図3(b)に示します.
図3(b)を見れば分かると思いますが,ポイントとなるのはメソッド呼び出 し,つまり(b2)から(b3)に移る時です.この時,オペランドスタックに詰まれ ている引数をローカル変数へコピーする作業は,この実装例ではコピーするの ではなく,フレームの一部を重複させることで対応します.リターンの方は戻 り値の書き込みが必要です.
フレームについては,何もこの図のような囲みが存在するわけではなく, フレームの操作に必要なポインタを何カ所か保持するだけです.多分必要なの は次の二つでしょう.
もちろんここで挙げた方式はあくまで一つの実装例に過ぎず,これに従う 必要はありません.特にインライン展開を行う場合は,このまま実装する のは効率の面から好ましくないでしょう.
参照に対応したモニタの取得,解放を行う命令です(表3).オペランド スタック上の参照に対応したモニタを取得/解放します(図4).
ニモニック | オペランド | 解説 |
---|---|---|
monitorenter | なし | 参照に対応したモニタを取得する |
monitorexit | なし | 参照に対応したモニタを解放する |
これらの命令はJava言語では,通常は(non-staticの)synchronized文の 実装に用いられます(注15).
注15:synchronizedメソッドをmonitorenter/monitorexitで実装する ことも可能なはずだが,コードサイズが大きくなるなどのデメリット があるので,通常はまず行われない.
Javaではsynchronizedメソッドの使用が推奨されており,その時のモニタ の取得はメソッド呼び出し命令で行い,解放はリターン,もしくは例外処理で 自動的に行われます.そのためmonitorenter及びmonitorexit命令の使用頻度 は低くなるはずで,そういう意味ではあまり重要な命令ではありません.
すでに少し触れましたが,staticなsynchronized文はJavaVMのレベルでは サポートされておらず,バイトコードコンパイラがそれ用のstaticメソッドを 作成することで対応するようです.このメソッドはソース中にないメソッドな ので,synthetic属性を使用する例でもあります.
このような方式になっているのはクラスファイルの互換性を保つためだと 考えられます.monitorenterとmonitorexitは参照に対応したモニタの取得/ 解放しか行えず,staticなsynchronized文で必要となるクラスに対応したモニ タの取得/解放を行う機能はありません.monitorenter,monitorexitの仕様を 変更,もしくは新しい命令を追加すれば対応は容易ですが,そうするとクラス ファイルの互換性がなくなります.
synchronized文は昔から使われているロック変数に毛が生えた程度の代物 で,その使用は推奨されていません.このため使用頻度は少なくなるとはずで, そんなもののためにクラスファイルの互換性を失うのは無意味でしょう.
バイトコードの説明も残すところあと僅かになりました.次回で今度こそ 本当にバイトコードの説明を終了し,その次からはベリファイに進めると思い ます.
なお,原稿執筆中に待望の「Java仮想マシン仕様 第二版」がピアソンエデュ ケーションより発売されました.訳もこなれてきている上に,値段が初版の半 額以下の4000円と随分お買得になっています.これもJavaがブームになったお かげでしょう.昔からJavaVMに関わっている者としては感無量という所です.
synchronizedメソッドと深く関係する問題として,いわゆる" double-checked lockingイディオム"の問題があります.このイディオム はSingletonパターンの実装を高速化するために広く使われた有名なイディオ ムでしたが,実は致命的な欠陥があり,現在のjavaのメモリモデルで は正常に動作する保証がありません.詳しくはdeveloperWorksの次 の記事を参照してください.
結論から言えば「double-checked lockingイディオムは正しく動作しない ので,使ってはいけない」ということになります.javaのメモリモデル自体の 見直しも行われているようですが,極めて影響の大きい変更だけに,もう少し 時間がかかるようです.JSR133においても言語のセマンティクスに変更を加え る予定はないようなので,おそらく今のままのdouble-checked lockingイディ オムが再び日の目を見ることはないでしょう.
上の記事はJavaVMの最適化や実装依存部分がどのようにJavaVMの挙動を変 化させ得るかという点においても興味深い記事です.JavaVMに関する知識を深 めたい方は必見と言えるでしょう.また並列プログラミングがいかに難しいも のでかを理解する上でも,貴重な失敗事例だと思います.なお私自身は並列プ ログラミングは一通り勉強しましたが,このイディオムの落とし穴には気付き ませんでした.
排他制御にはモニタやミューテックスロック(以下単にロック)などがあり ます.ロックが最も基本的なもので,フラグのON/OFFでのみ処理されます.ロッ クを獲得できたただ一つのスレッド/プロセスのみがそのまま実行され,それ 以外はロックの獲得に失敗します.獲得に失敗した時にどうなるかはそのシス テムコールに依存します.ロックが獲得出来るまでスリープすることもありま すし,失敗後即座に復帰し別の処理をさせる場合もあります.Javaでも使われ ているモニタは,ご存知の通りもっと複雑なものです.多くの場合はロック以 外の排他制御はロックを使って実装することが多いようです.(逆も出来なく はないが,ほとんど無意味.)ロック自体の実装はtest-and-set命令のような ハードウエアサポートを必要とします.そのような機能のないハードウエア上 では実現できません.
Javaの排他制御の特徴は,やはり安全ということに尽きるでしょう.例え ばモニタの取得回数と開放した回数が異なることはありません.(mallocした のにfreeを忘れるのと同じで,普通のモニタやロックでは取得したあと開放を 忘れる可能性は常にある.ただしメモリリークと異なり,それを避けるのはさ ほど難しくない.) 例外処理が発生した場合には獲得していたモニタは必要に 応じて自動的に開放されます.ある意味当たり前のことのようですが,古典的 なロックやモニタでは必ずしも実現されていませんでした.
このように便利になった反面,排他制御のイロハも知らない人がマルチス レッドを使ってたりするわけで,その分見えない所でのトラブルも多くなって いるのではないかという心配はあります.特にJ2EE分野ではマルチスレッドが ベースになってますし,理解してなくてもそれなりにプログラムを書くこと自 体はできてしまいます.しかしマルチスレッド周りではタイミングバグのよう に再現性の低いバグが多く,一度や二度走らせた程度ではバグの発見すらでき ません.またデバッガを使うとバグが出現しなくなるなど,デバッガが全く役 に立たないことも珍しくありません.
マルチスレッドプログラミングの基本は,理論的に正しく動作するように 設計しテストは理論通りに実装されているかを試すためという位置付けにすべ きでしょう.間違っても適当に実装したものを2〜3回走らせて問題が出なけ ればそれで良いなどとは思わないように.(信頼性が低くても構わない場合は それでも良いのかもしれませんが...)
テストする環境も重要で,並列実行することが分かっている場合は可能な らば実行する環境と全く同じものを用意するのが理想です.そこまでいかなく ても,最低でも3CPU以上のマシンでテストすべきでしょう.1CPUでバグが出な くても2CPUでなら表面化するバグがあるのは容易に想像できると思いますが, 2CPUで出なくても3CPUで出るバグもさほど珍しいものではありません.3CPUで 出なくても4CPUなら出るバグというのもありますが,経験的に言ってその数は かなり少なくなります.また,このような場合はアルゴリズム自体に問題があ ることも多く,アルゴリズムがきちんと設計されていれば,多くの場合は回避 できます.(例えばCPU数を4で割った「余り」で,他の数を割ろうとしてエラー になるというのがその一つ.これはマルチスレッドがどうこうという前に,0 除算が起こり得るというアルゴリズム自体の問題.)
上のdouble-checked lockingのアルゴリズム上のバグなども,幅広く使わ れていたにも関わらずなかなか判明しませんでした.このバグは,1,最低で も2CPU以上のハードウエア上で,2,初期化直後に他スレッドから利用される 可能性がある,3,ある特定の問題が出るJavaVM実装を使っている,の条件が 揃わないと,表面化しないでしょう.実際にはこれでも表面化する可能性は低 く,4,コンストラクタが巨大なためにインスタンスの初期化に時間がかかる, 5,その初期化が終わる前に処理することが即トラブルに繋がる,などの条件も 必要かもしれません.
(ここで「ある特定の問題が出るJavaVM実装」と書いているが,これは「そ のJavaVM実装にバグがある」ということは意味しません.JavaVM仕様に準拠し ている限り,それは正しいJavaVMです.問題なのはJavaの仕様に準拠していな い,double-checked lockingイディオムのアルゴリズム上のバグの方にありま す.或いは,このような扱い難い落とし穴を発生させ得るJavaVM仕様,特にそ のメモリモデルの側の未熟さという問題もあります.「欠陥」とか「バグ」という ことはないものの,「扱い難い」ということは必ずしも好ましいものではあり ません.)
もう一つの問題はパフォーマンス上のバグですね.これはいわゆるパフォー マンス=チューニングではなく,アルゴリズム的な問題のこと.厳密にはバグ と呼ぶべきかどうかは難しい所だが,便宜上こう呼ばれることも多いと思う. これはスケーラビリティーを完全に無くしてしまう点が恐ろしい.
n個のCPUがあるマシン上では1個のものより最大でn倍の性能が出るはずだ が,実際にn倍の性能が出ることは珍しい.(もちろんアイドリング等は除いて 考える.)例えば10個のCPUを使っておきながら3倍しか性能が出ないというこ とも珍しくない.これがパフォーマンスバグがある場合,最悪では並列マシン の方が1CPUよりも数段遅くなることもありうる.排他制御というのは例えるな ら高速道路における料金所のようなものです.10車線の高速道路に1車線の料 金所があれば,そこを基点に大渋滞を起こすのは容易に想像できると思います. こうなると料金所以外の車線数をどれだけ増やしても問題は解決しません.
このようなバグは並列マシンで相当な負荷をかけない限り表面化せず,通 常のテストでは検出できません.ハードウエアのアーキテクチャやOSやJavaVM の実装によっても異なります.もちろん再現性も低く,ある特定条件が重なっ た時にしか発生しない場合もありえます.ある意味でメモリリークのようなも ので極めてデバッグが困難な性質の悪いバグの一つです.ただしメモリリーク とは違って,これを未然に防ぐことはさほど難しくありません.マルチスレッ ド=プログラミングの基本さえ理解していれば,滅多に問題になることはない でしょう.
このようなバグを未然に防ぐために,マルチスレッド=プログラミングでは 同期は必要最小限になるように設計するのが鉄則です.どうやら逐次処理しか 経験のないプログラマーには,これがなかなか理解できないようです.Vector やHashtableのようなMT-Safeなクラスの危険性に気づかず,平気で利用したり します.(MT-Safe,multi-thread 安全なクラスは,マルチスレッド環境下で はほとんど使い道が無い.ちなみにこれらを使っているからと言って,必ずし もマルチスレッドで本当に正しい動作をしてくれるとは限らない.)Vector と Hashtableは使わない方が無難です.特にマルチスレッド環境では絶対に使う べきではありません.これらのクラスがレガシー=コレクションとまで言われ ているのには,それなりの理由があるのです.
なおsynchronizedメソッドやその他の排他制御自体の処理速度はJavaVMの 実装によって異なります.一般的に言って最近の高度に最適化されたJavaVM ではメソッド自体の処理速度はまず問題にはなりません.通常はsynchronized メソッドがそうでないメソッドより遅いのは事実ですが,よほど頻繁に利用す るのでない限りまず気にする必要はないでしょう.
次に参考になるページへのリンクを挙げておきます.
JavaVM仕様書第二版が「たったの」4千円で入手可能になったのはありがた いことです.多くの方が指摘しているように,技術を身に付けたければ良い本 に対する出費くらいは惜しむべきではありません.娯楽に万単位で金をつぎ込 むこと自体は否定はしませんが,その1割やそこらを自分の能力開発に投資す るくらいは当然だと私は考えています.良い本が与えてくれる知識に比べれば その出費など微々たるものです.それができないようなら「生涯学習」などと いう言葉も虚しいだけです.
もちろん「プログラミング言語Java 第三版」も買いましたよ.悩みに悩ん だ末,原書第三版を買った直後に日本語版が出た時は,嬉しいような悲しいよ うな....まあバイトの方が有効活用しているようなので,良しとしましょ う.
ちなみに書籍の再販制度維持については私は必ずしも賛成ではありません. 再販制度が維持されることと日本での知的活動が維持されることとは,ほとん ど無関係です.今の再販制度下では結局横並びの評価しかなされず,良書を書 く努力が報われません.たとえ優れた才能と豊富な経験の持ち主が十倍の労力 と経費を費やし,3倍の価値の書籍を執筆したとしても,それに対する報酬は 適当な孫引きをツギハギにした三流の入門書と同じかそれ以下となれば,一体 誰がその努力をするでしょうか.(或いは誰にその犠牲を強いるべきでしょう か.)実際にも,日本語書籍で売れる本は,軒並み入門書というのが現状だと 思います.高度なものほど翻訳,或いは執筆が困難であるにも関わらず,売れ る数は少なくなる.となると一冊あたり数万円という値段を付けない限り採算 が合わなくなるが,技術者の給与というのがそれに見合うほど高いわけでもな い.これでは洋書を英語のまま読むというのが現実的な解になるのも当然です ね.
(このような状況なので,一流の技術者,研究者には英語が堪能な人が多く なったとしても驚きはしない.一流でありつづけるためには常に最新の情報を 学習しつづける必要があるが,そのためには専門書を原書で読める程度の英語 力が必要不可欠.英語を使えることによって「一流になる」のではなく,英語 が使えないと「一流ではいられなくなる」.英語書籍にも下らないものがある のは事実だが,名著と言われるものに限っても質,量共に日本語書籍を圧倒し ている現実がそこにある.)
新刊書店には価格決定権がなく(さらに言えば仕入れや品揃えにも主導権が ない),これは小売業としては致命的だというブックオフ社長の坂本氏の意見 には賛成します.これに対しブックオフの逆オークションのようなやり方は, 客が値段を決める方式と言える.(というより,そう主張していたと思う.)情 報は生ものであり,鮮度が重要.古くなった本の価値が新品と同じという考え 自体が,むしろ異常だといえるでしょう.特にIT関係の専門書はこの傾向が強 く見られます.ヘネシー&パターソンのような名著ならいざしらず,ほと んどの本は古くなると共に価値が失われていき,僅か数年で価値がほとんど0 になる本が続出いうのが現状でしょう. [ブックオフの真実]
ちなみにブックオフのことを「寄生虫のようなものだ」とする表現もある ようだが,実はある意味でこれは正しい.サナダムシなどの寄生虫というのは 本来その宿主にほとんど害を与えない.害を与えるのは寄生虫の宿主への適応 が不十分な場合で,その多くは本来とは異なる宿主に間違って寄生した場合に 限られる.(これは宿主にとってだけでなく,寄生虫にとっても災難だったり する.) 例えば本来は魚に規制する寄生虫が人間の体内に入った場合などがこ れにあたる.人間に寄生する寄生虫が人間に寄生してもほとんど害は無い. (寄生虫ではないが,SARSもこのパターンが疑われている.本来はハクビシン など人間以外に感染するウイルスが,誤って人間に感染したがために通常より 高い致死性を示していると考えられる.)
それどころか寄生虫がいなくなる方が体のバランスが崩れ,寄生虫がいる 時よりもむしろ不健康になるという報告さえある.例えば花粉症などのアレル ギーもその一つ.花粉症でお悩みの方は,一度寄生虫を1匹,お腹で飼ってみ てはいかがでしょう?それだけで嘘のように直る例もあるそうだし,少なくと も薬物と違って副作用の心配はほとんどない.
これはハイエナについても同様で,ハイエナも生態系における必要不可欠 な存在として機能している.それこそ,もしハイエナが絶滅したりすれば,ハ イエナに依存している生態系に深刻な影響を与えかねない.(そもそもハイエ ナだって必要とあればライオンだって襲って食べるというし.屍肉を食べるの はそちらの方が合理的だからであって,屍肉でなければならないというわけで さえないらしい.)
http://www.netgene.co.jp/