NetBeansプロファイラーを使ってみる(パフォーマンス測定)

プログラムの中で時間のかかる処理を調べる場合にプロファイラが活躍します。私はプロファイラの機能の中でも、このパフォーマンス解析を一番使っています。

時間のかかる処理

時間のかかる処理のサンプルプログラムというのは、いい例が作成しにくいのですが、
以下のようなサンプルを挙げてみました。

  • Listに10万件のランダムなIntegerオブジェクトを追加
  • 10万件のListをループして、全部加算する
  • 加算した結果をラベルに表示する

これを実行するサンプルプログラムを作成しました。

画面デザインはこんな感じで、ボタンを押した時にListの作成と演算を行い、ラベルに計算結果を表示します。
画面デザイン
 ↓
実行結果
ボタンを押した時のロジックはこんな感じにしてみました。
ただし、パフォーマンスを劣化させるための悪いロジックです。


private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
  List<integer> list = new ArrayList<integer>();
  Random rand = new Random(20070101);
  for (int i = 0; i < 100000; i++){
    list.add(0, rand.nextInt(100));  //Listの先頭に挿入?
  }
  long total = 0;
  for (int i = 0; i < list.size(); i++){
    total += list.get(i).intValue();
  }
  jLabel1.setText("計算結果:" + total);
}

実行してみると、私の非力なノートパソコンでは、9秒くらいかかってしまいます。これをプロファイリングしてみます。

プロファイリング

メニューの[プロファイル]-[主プロジェクトをプロファイル]で、「パフォーマンスを解析」を選択します。
パフォーマンスを解析
プログラムのどの部分を解析するかを絞り込むことができます。とりあえず全部解析してみましょう。
プロファイリング自体に大きなオーバーヘッドがかかるので、箇所が分かっている場合には特定のクラスやメソッドにしたほうがスムーズに進みます。
「実行」ボタンを押します。

普通に実行したときと同じようにアプリケーションが起動するので、パフォーマンスを解析したい操作を行います。この場合は「計算する」ボタンを押すだけです。普段よりもちょっと遅く感じるのは解析をしているからです。

計算し終わったら、アプリケーションを終了します。
プロファイリング結果が表示されます。
プロファイリング結果1
この結果を見ると、jButton1ActionPerformedメソッドで9.1秒かかっていて、その中で呼び出されているArrayListのaddメソッドの呼び出しが8.3秒かかっています。(10万回呼び出しの合計)
つまり大部分は、このaddにかかっている時間なわけです。その他の処理はaddメソッドに比べたら微々たるものです。

プログラムの修正

addメソッドが遅い原因はArrayListの先頭に挿入しているためです。ArrayListは配列としてリスト構造を保持しているため、挿入すると、後続の要素を後ろにずらすためのコピーが発生します。先頭に挿入すると、全部の要素を1つずつ後ろにコピーしなければならないのです。それが10万回発生するわけですから非効率極まりないです。
末尾に追加した場合はずらす処理が発生しないので、このような無駄な処理はなくなります。

プログラムを修正します。


for (int i = 0; i < 100000; i++){
  //list.add(0, rand.nextInt(100));  //修正前
  list.add(rand.nextInt(100));  //修正後 Listの末尾に追加
}

末尾に追加して同様にプロファイリングしてみます。
プロファイリング結果2
8.3秒掛かっていたaddメソッド10万回が、0.2秒に短縮されています。約30倍速くなりました。このことで計算処理時間は0.9秒に短縮されました。

別の修正方法

その他の修正を考えてみます。
ArrayListは挿入が苦手ですが、連結リスト構造をもつLinkedListは挿入が削除が得意です。(なぜ得意かはここでは説明を省きます。連結リスト構造について調べてください)先頭に挿入するという処理をそのままに、LinkedListでパフォーマンス向上してみます。

再びダメダメなコードに戻し、ArrayListの代わりにLinkedListを使うようにしてみます。


List<integer> list = new LinkedList<integer>();
Random rand = new Random(20070101);
for (int i = 0; i < 100000; i++){
  list.add(0, rand.nextInt(100));  //Listの先頭に挿入?
}

LinkedListは挿入が得意なので、先頭に挿入しても高速に動作するはずです。これで試してみましょう。
あれ?なんか前よりもパフォーマンスが劣化したようです。23秒もかかってしまいました。
プロファイリング結果3
プロファイリング結果を見ると、LinkedListを使うことで確かにaddのパフォーマンスは向上しています。以前は10万回のaddが8.3秒掛かっていたのが、0.2秒になっています。ところが、getメソッド10万回の呼び出しに22秒もかかっています。ArrayListnの時は0.2秒程度しかかからなかったはずなのに…。

連結リストは挿入・削除は得意ですが、インデックスを指定して要素を取り出すことは苦手です。100番目の要素を取り出すために、0番目から順にたぐっていかなければならないからです。インデックスを指定せずにIteratorを使ってスキャンすれば高速にアクセスできます。
合計値の算出部分をIteratorを使うように修正します。


for (Iterator<integer> itr = list.iterator(); itr.hasNext();){
  total += itr.next().intValue();
}

これで再びプロファイリングしてみます。
プロファイリング結果4
getメソッドは使わなくなり、全体の処理時間は0.9秒になりました。
ArrayListで先頭に挿入しなかった場合とほぼ同等です。
下記のように拡張for文を使っても同様のパフォーマンスになりました。

for (Integer v : list){
  total += v.intValue();
}


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)