Wednesday, January 16, 2008

Java 例外のコスト

気になったので次のようなコードで計測してみた。JDK のバージョンは 1.5 。


public static void main(String a[]) throws Exception {
int c = 0;
System.out.println(new Date());
for (int i = 0; i < 1000000000; i++) {
c = test(0);
}
System.out.println(new Date());
System.out.println(c);
}

static int test(int a) {
int c = 0;
if (a == 0) {
String s = new String("13");
return Integer.parseInt(s);
} else {
return c + 1;
}

}

ここでは、軽い仕事をして普通にリターンしている。


public static void main(String a[]) throws Exception {
int c = 0;
System.out.println(new Date());
for (int i = 0; i < 1000000000; i++) {
try {
c = test(0);
} catch (Exception e) {
c = i;
}

}
System.out.println(new Date());
System.out.println(c);
}

static int test(int a) throws Exception {
int c = 0;
if (a == 0) {
throw new Exception();
} else {
return c + 1;
}

}

ここでは、何もせずに例外をスローすることでリターンしているのだが、前のコードの 16 倍 の時間がかかった。Exception をその都度生成するのではなく、static に確保しておいて、それをスローするだけにすると時間はほぼ同じだった。

結局、例外の throw/catch そのものは大したことないが、例外オブジェクトの生成に相当のコストがかかることが分かる。この理由は、例外オブジェクトの生成時にスタックトレースのスナップショットをとることにある。これが高くつくというわけだ。(パフォーマンスへの目: 開発プロセスを改善する 中の例外のコスト)

したがって、パフォーマンスの面からだけでも、例外は例外状態のみに使用すべきであり、正常フローには使っていけないということになる。そして、正常フローでの使用を避けるために状態チェックメソッドや、null 等の区別できる値を返したりする。

ところで、例外がサポートされていない言語では、関数内でエラーが起こったときにそのエラーの種類を複数のエラーコードで返すようになっていることがよくある。

この場合、例外が使えるならば、回復可能と考えられるエラーにはチェックされる例外、そうでないエラーには実行時例外を対応させて例外クラスを定義し、エラーが起こったらそれをスローすることになるだろう。そうすれば、エラーコードと違って、呼び出し側に対応を強制することができ、コードの信頼性・保守性が向上する。これはまさに正しい例外の使い方といえる。

ただ、このようなメソッドでは例外状態であるにも関わらず、それが頻繁に発生するものがある。例外の生成には非常にコストがかかるので、これは困ったことであり、コードの信頼性・保守性とパフォーマンスとが両立しない状態になってしまっている。

このとき、例外を使わずにすまそうとするなら、状態チェックメソッドの導入が考えられるが、エラーが複数ある場合にはそれぞれに状態チェックメソッドが必要となり、メソッドが使いにくくなるし、成功か失敗かを判定するのにデータベースへのアクセスが必要だったりすると逆にパフォーマンスが悪くなってしまったりする。

これの解決案としては、例外のコストは例外オブジェクトの生成が大部分であり、throw/catch にはほとんどかからなかったことから、例外オブジェクトをあらかじめ static で作成しておき、それを使いまわすということが考えられる。

ただ、これをやると、スタックトレースの取得が不可能になるのと、(マルチスレッドで使用されるときには)エラー発生時の状況を例外オブジェクトに組み込むことができないという代償はある。

Friday, January 04, 2008

Linux を CD ブートして、ハードディスクの障害を復旧する方法

障害というほどではないが、実験中に重要な共有ライブラリ(libcrypt)の名前を変更してからルートを抜けてしまい、戻せなくなってしまった。sudosu もこのライブラリを使っているのでルートになることも新たにログインすることもできない。

そこで、インストールに使った Ubuntsu の CD からブートして復旧を試みた。当然、BIOS の設定はハードディスクより CD を優先してブートするようになっていなくてはならないが、幸い、これは問題なかった。

Ubuntsu のインストールCD でブートすると、最初にメニュー画面が出てきて Rescue Mode のような選択肢が選べるようになっている。とりあえずこれを選んでみたが、ファイル名を元に戻すだけなので、すぐに Alt-F2 のようにして別コンソールに移動して、シェルに抜けた。そこで


# mkdir /a
# mount -t ext3 /dev/sda1 /a


のようにして問題のディスクをマウントし、ファイル名を修正した。ちなみに /a は RAM ディスク上に作られている。

後は umount してリブート、CD をイジェクトして終わり。うまく元通りにできた。

Thursday, January 03, 2008

PostgreSQL 8.2.5 のインストール

PostgreSQL 8.1.5 をインストール と同様にソースからビルドしてインストール。実は以前インストールしていたのを忘れてて、再度インストールし直した。8.1.5 はシステムから削除した。

8.1.5 同様にインストールしたが、ライブラリまわりのトラブルは解決しておいたので、今回はスムーズにビルド&インストールがうまくいった。ただ、インストールやリグレッションテストのときにファイルの権限を PostgreSQL 用の管理ユーザに変更して実行するのは面倒なので、最初から PostgreSQL 用の管理者ユーザでビルド&インストールした。

つまり、postgres のホームに 8.2.5 のソースを展開し、

./configure --prefix=/opt/pgsql-8.2.5
make
make install

を実行。/opt/pgsql-8.2.5 はあらかじめルートで作成しておき、

# chown -R postgres:postgres pgsql-8.2.5

を実行しておいた。/opt/pgsql-8.2.5/ には /opt/pgsql というシンボリックリンクを張り、これを環境変数 PGDATA にセットするようにした。

リグレッションテストだが、インストールする前に最上位ディレクトリまたは src/test/regress/ ディレクトリで

gmake check

で実行できる。インストールしてから、src/test/regress/ 内で実行したとき、FAILED が結構出たのだが、make clean を実行してから再度実行するとうまくいった。どういう原因だったのかは分からない。

インストール後、管理者ユーザで

initdb -E UTF8

でデフォルトエンコーディング UTF8 でデータベースクラスタを作成し、

postgres

でデータベースサーバを起動できる。アーキテクチャが変更され、postmaster コマンドは廃止されている。サーバを起動すると、/src/test/regress/ で

gmake installcheck
gmake installcheck-parallel

とすれば、インストール後にリグレッションテストができる。パラレルチェックでは複数プロセスを起動して、テストスクリプトを並行に実行する。

Wednesday, January 02, 2008

java.util.List の size() メソッドをループの範囲チェックでは呼び出さない方がいい

List をループで使用する場合、次のようなコードを書きがちだ。

List li = new ArrayList();
// この辺は省略
for(int i = 0; i < li.size(); i++) {
Object o = li.get(i);
}

これは、次のように書いた方がいい。

List li = new ArrayList();
// この辺は省略
for(int i = 0, n = li.size(); i < n; i++) {
Object o = li.get(i);
}

僕のマシンでこのループを 10億回まわしてみたところ、ループの都度 size() を呼び出すと 43秒ほどかかり、初期化のところで1回だけ呼び出すようにすると 25秒ほどかかった。

修正後の所要時間は修正前の 58% にまで下がっている。

また、ここでは List の要素にアクセスするのにイテレータではなく、get(int) メソッドを使っている。これがふさわしいのは、List の実装がランダムアクセスの場合だけだ。LinkedList などの場合は、ランダムアクセスではなくシーケンシャルアクセスなので、イテレータを使うべきだ。

以上のことは、Joshua Bloch 著 『Effective Java』 の29項に触発されて調べてみた。