[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[jfriends-ml 10110] Re: Java 言語で学 ぶデザインパターン入門第 6 回議事録



村山です.

> モニタはロックとそれを獲得しようとするスレッドの待機セット
> で、オブジェクトが持っているものですから、
> スレッドが取得しようとするのはロックではないでしょうか?

並列処理における排他制御としては「ロック」「セマフォ」
「モニタ」は全て別物です.単なるロック(ミューテックスロック)
は最も原始的な排他制御機構で,ロック変数のON/OFFをatomic
(不可分)に行う処理(通常はOSのシステムコール)なはずです.
#で,多分いいはず.

ただ,もっと一般的な意味で「ロック」という単語使うこともない
わけではないので,必ずしも間違いとは言い切れません.まあ
「インスタンス」のことを「オブジェクト」と呼ぶようなもので,
間違いじゃないけど誤解を招きかねない表現なんで,避けた方が
無難だと思います.
#あるいは「指し示すもの」という意味で「ポインタ」を使うとか.

> あと、同じスレッドが同じロックを獲得するのにデッドロック
> になるかどうかは、ロックの実装によるはなしで概念ではない
> と思うのですが、どうなんでしょうか?
> #pThredとかはだめなんでしたっけ?

通常「ロック」は単にON/OFFなんで,どのスレッドが取得したとか
は全く認識しません.異なるスレッドだろうが同じスレッドだろうが,
二度目を取ろうとするとロックを獲得できず,下手するとその時点で
待ちに入ってデッドロックになります.待ち状態に入ったが最後,
自分が持ってるロックを開放する術はないですからね.

これがJavaのsynchronizedメソッドなんかだと,同じスレッドで
何回獲得しても,自動的にカウンタのインクリメントをして
対応してくれるので,シングルスレッドによるデッドロックなんて
のは起こりません.おまけに例外が発生した場合でも,必要に応じて
自動的にモニタの開放をしてくれますし.これに対し「ロック」の
場合はこれら全てを自分で管理する必要があります.
#C言語のポインタバグなんかもそうですが,
#Javaの場合,そういった初歩的なバグが絶対起こらないので,
#Javaから入った人にはこの苦労はわからないでしょうね.

> そうなのかよくわからないので、試してみました。
  中略
> もちろん、僕のマシンは1CPUです。うーん。なんでこないだのプログラム
> は差がつかなかったのに、今度はついたのかわかりません。
> #しかも平均しても2.5倍くらいはやくなっている。。。。
正直言って,この結果は予想してませんでした.

一つの可能性ですが,大域的な最適化を行っているのでは.

---
b.append(foo);
b.append(bar);
b.append(baz);

の場合,
aload_1
getfield #10
invokevirtual #20
getfield #11
invokevirtual #20
getfield #12
invokevirtual #20
って感じになると思いますが,これがインライン展開されて,

aload_1
getfield #10
モニタ取得
appendの処理
モニタ開放
getfield #11
モニタ取得
appendの処理
モニタ開放
getfield #12
モニタ取得
appendの処理
モニタ開放

となって,
「getfieldくらいなら中にいれちゃっても,まあいいか」
と判断して,
#入れるのはそれほど問題ないが,出すのはバグになると思う.

aload_1
モニタ取得
getfield #10
appendの処理
モニタ開放
モニタ取得
getfield #11
appendの処理
モニタ開放
モニタ取得
getfield #12
appendの処理
モニタ開放

そうすると連続する取得と開放は無意味だし,この程度
なら連続して実行しても許容範囲だと判断して,

aload_1
モニタ取得
getfield #10
appendの処理
getfield #11
appendの処理
getfield #12
appendの処理
モニタ開放

となってる「かも」しれません.この場合は前後にモニタを
くっつけても,同様にして取り除かれるので,全体としての
速度は同じになるでしょう.

ところが,
    for (int i=0; i<v.size(); i++) {
        String s = (String) v.elementAt(i);
        if (s.length() > maxLen) {
        maxLen = s.length();
        longest = s;
        }
    }

ではそういう風な処理はできないんですよね.順番を入れ換えると
アルゴリズムも変わってしまい,バグが出る恐れがあるので.

で,これに対する最適化も,「v.elementAt(i);」の前後に跨る
ような最適化は不可能になる.簡単に言えば次の1,2,3の部分
はそれぞれ独立に最適化するしかない.
#クラスファイルレベルじゃ,必ずしもこの順序で並んでいる
#わけではない.あくまでイメージとして捕らえてください.

1, for (int i=0; i<v.size(); i++) {

2,     String s = (String) v.elementAt(i);

3,     if (s.length() > maxLen) {
        maxLen = s.length();
        longest = s;
        }
    }

これが,次のようになると,
    synchronized (v) { 
        for (int i=0; i<v.size(); i++) {
        String s = (String) v.elementAt(i);
        if (s.length() > maxLen) {
            maxLen = s.length();
            longest = s;
        }  
    }  
「v.elementAt(i)」の段階でモニタを保持していることが保証
されているし,synchronized文は全部まとめて実行してもロジックの
上で問題がないことが保証されるので,ループ全体の最適化も可能に
なる.(と思う.)

たとえば

1,v.elementAt(i)のインライン展開
2,冗長なモニタの取得/開放処理の削除
3,モニタ取得のその前後に跨る,ループも含めた最適化
#「ソフトウエアパイプライニング」とか「ループアンローリング」
#とかいう有名な手法があったと思う.

なんて感じで最適化するのでは.

まあ,こういうことが起こるのも,同期の取り方自体もアルゴリズム
の一部だからですね.アルゴリズムが異なるのだから,処理時間
が異なっても当然でしょう.
#上のようなことまで考えて「最適化」しても,JavaVMが変わると
#元の木阿弥.「実装依存の最適化」は無駄な努力以外の何物
#でもないと思う.


ところで,
b.append(foo);
b.append(bar);
b.append(baz);
c.append(foo2);
c.append(bar2);
c.append(baz2);
と
b.append(foo);
c.append(foo2);
b.append(bar);
c.append(bar2);
b.append(baz);
c.append(baz2);
だと,やっぱり性能が変わったりするんですかね?
#素直にs1 = foo+bar+baz;とか書いた方がましかも.(-_-)
#だいいち,こっちの方が書くのが楽.

> パフォーマンスチューニングの道は険しいみたいです。
#うーん.まさにFINAL FRONTIER.

あ,ちなみにJalapenoの記事はお勧めです.
http://www-6.ibm.com/jp/developerworks/java/jalape-vm-index-j.html

結構難易度は高めですが,これが理解できない人は下手な
最適化はしない方が無難だと思います.