本文へジャンプ

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

送信されたすべての情報は安全です。

  • 閉じる [x]

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


送信されたすべての情報は安全です。

  • 閉じる [x]

Javaコードの診断: Javaコードのパフォーマンスを向上させる

末尾再帰変換はアプリケーションの速度を向上させる可能性はあるが、すべてのJVMで可能な操作ではない

Eric Allen, Software Engineer, Cycorp, Inc.
Eric Allen氏は、コーネル大学でコンピューター・サイエンスと数学の学士号を取得しています。現在は、ライス大学の博士課程の大学院生としてJavaプログラミング言語チームに加わっています。学位を終了するためにライス大学に戻るまでは、Cycorp, IncでJavaソフトウェア開発主任として勤務していました。彼は、JavaWorldで「Java Beginner」ディスカッション・フォーラムの司会者も務めています。主な研究対象は、Java言語のセマンティック・モデルと静的分析ツールの開発であり、いずれもソース・レベルとバイトコード・レベルで研究しています。Ericは、NextGenというプログラミング言語 (汎用ランタイム型によるJava言語の拡張版) のためのライスのコンパイラーの開発にも携わってきました。連絡先は、eallen@cs.rice.edu です。

概要: 多くのアルゴリズムは、末尾再帰メソッドとして表現するのが最も簡潔です。コンパイラーは末尾再帰メソッドを自動的にループに変換し、プログラムのパフォーマンスを向上させることができますが、この変換はJava言語仕様によって必須とされてはいないので、必ずしもすべてのJVMがこの変換を行うわけではありません。つまり、Java言語で末尾再帰メソッドを使用すると、予期しないほど多くのメモリーが使用される可能性があります。この記事でEric Allenは、動的コンパイルでは言語のセマンティクスが維持され、静的コンパイルでは多くの場合、セマンティクスが維持されないことを示します。また、なぜこうしたことが起こるのかを説明します。使用するジャストインタイム (JIT) コンパイラーがセマンティクスを維持しながら末尾再帰を変換できるコンパイラーであるかどうかを判別できる、短いコードも紹介します。

日付:  2001年 5月 01日
レベル:  初級 この記事の原文:  英語
アクティビティー: 4628 ビュー
お気軽にご意見・ご感想をお寄せください: 


末尾再帰と変換

多くのプログラムにはループが含まれており、これが合計処理時間のかなりの部分を占めます。多くの場合、これらのループは複数の変数を繰り返し更新し、また各変数の更新は更新される他の変数の値に依存します。

コードへ直行

リスト1. 複数の整数 (Integer) の集合 Iterator のエレメントの乗算の失敗 このプログラムにはバグが含まれ、実行時に例外がthrowされます (この例外は重要な手掛かりとなります)。

リスト2. 不適切な呼び出しを捕そくする試み Example2 クラス内のオーバーライドされた productHelp メソッドは、 productHelp への不適切な呼び出しを捕そくしようとしますが、これによって新しいバグが作られています。

リスト3. 言語のセマンティクスの維持には、静的コンパイルより動的コンパイルが良い このコードを2つの異なる静的コンパイラーでコンパイルすると、一方から生成されたコードは例外をthrowし、他方から生成されたコードは例外をthrowしないという場合があります。このため、混乱します。

リスト4. 使用するJITは、言語のセマンティクスを維持しながらコードの末尾再帰を変換できるか ? この変換を行えるJITであるかどうかを判別する方法を示すサンプルです。

リスト5. 動的変換 このコードは、十分な機能があるとされているコンパイラーが行う変換を表しています。

これらの変数は、反復が末尾再帰関数 として表現されている場合、関数に渡されるパラメーターとして表現されます。(注意: ある再帰呼び出しの戻り値が、その関数を呼び出す側の関数の値として直接戻される場合、その呼び出しを末尾再帰呼び出しと言います。呼び出しを行うときに呼び出し元関数のコンテキストを記憶している必要はありません。)

こうした特性があるため、末尾再帰関数とループの間にはちょうどよい対応関係があります。つまり、それぞれの再帰呼び出しは、単純に、ループをもう1回反復することであると考えることができます。しかし、変数パラメーターの値は一度に再帰呼び出しに送られるため、ループの場合よりも簡単にそれらの値を更新することができます。さらに、多くの場合、使いにくいブレーク・ステートメントの代わりに、関数からの単純な戻り値を使用することができます。

しかしJavaプログラミングでは、この方法で反復を表現すると、大量の再帰呼び出しによってスタックがオーバーフローする恐れがあるため、効率が悪くなる可能性があります。

解決策は比較的単純です。これらの末尾再帰関数は、実際には、ループをより簡単に書くため手段にすぎないので、コンパイラーによって末尾再帰関数が自動的にループに変換されればよいのです。それにより、両方の方法の良いところを十分に活用できます。

しかし、末尾再帰関数を単純なループに自動的に変換する方法はよく知られているにもかかわらず、Java仕様ではこの変換を行うことが要件となっていません。おそらく、これが要件となっていない理由の1つは、一般にオブジェクト指向言語ではこの変換を静的に行うことができないためであると思われます。したがって、末尾再帰関数から単純なループへの変換は、JITコンパイラーによって動的に行う必要があります。

なぜこのようになるのかを理解するために、次に、複数の整数からなる集合に、反復子のエレメントを乗算するサンプルのバグをあげます。

次のプログラムにはバグが含まれているため、実行時に例外がthrowされます。しかし、このシリーズの以前の多くの記事で示したように、プログラムがthrowする例外は、(バグの種類を識別する手掛かりであるのと同時に) バグがプログラムのどこにあるのかを判断するための貴重な手掛かりであるので、コンパイラーによってプログラムが変更されて、その変更されたコードが異なる例外をthrowするようになることは望ましくありません。


リスト1. 複数の整数 (Integer) の集合Iteratorのエレメントの乗算の失敗
                
import java.util.Iterator;

public class Example {

  public int product(Iterator i) {
    return productHelp(i, 0);
  }

  int productHelp(Iterator i, int accumulator) {
    if (i.hasNext()) {
      return productHelp(i, accumulator * ((Integer)i.next()).intValue());
    }
    else {
      return accumulator;
    }
  }
}

product メソッドのバグに注意してください。このメソッドはaccumulatorの値 "0" で productHelp を呼び出しています。accumulatorの値は "1" でなければなりません。そうしないと、クラス Example のどのインスタンスで product を呼び出しても、どのようなIteratorが渡されたかにかかわらず、結果が "0" になってしまいます。

リスト2に示すように、このバグはたまたま修正され、一方でクラス Example のサブクラスが作成されていたとします。


リスト2. リスト1のタイプの不適切な呼び出しを捕そくする試み
                
import java.util.*;

class Example {

  public int product(Iterator i) {
    return productHelp(i, 1);
  }

  int productHelp(Iterator i, int accumulator) {
    if (i.hasNext()) {
      return productHelp(i, accumulator * ((Integer)i.next()).intValue());
    }
    else {
      return accumulator;
    }
  }
}


// And, in a separate file:

import java.util.*;

public class Example2 extends Example {
  int productHelp(Iterator i, int accumulator) {
    if (accumulator < 1) {
      throw new RuntimeException("accumulator to productHelp must be >= 1");
    }
    else {
      return super.productHelp(i, accumulator);
    }
  }

  public static void main(String[] args) {
    LinkedList l = new LinkedList();
    l.add(new Integer(0));
    new Example2().product(l.listIterator());
  }
}

クラス Example2 のオーバーライドされた productHelp メソッドは、accumulatorが "1" 未満の場合に実行時例外をthrowすることにより、 productHelp に対する上記のような不適切な呼び出しを捕そくしようとします。残念ながら、このようにすることにより新しいバグが生まれます。 Iterator に "0" のインスタンスが含まれていると、 productHelp は再帰呼び出しを行ったときにクラッシュしてしまいます。

ここで、クラス Example2 のメイン・メソッドで Example2 のインスタンスが作成され、その product メソッドが呼び出されていることに注意してください。このメソッドに渡される Iterator には "0" が含まれるので、このプログラムはクラッシュすることになります。

ただし、クラス Example 内の productHelp は、確かに末尾再帰的であることはわかります。1つの静的コンパイラーが、リスト3のように、このメソッドの本体をループに変換したとします。


リスト3. 例: 静的コンパイルでは末尾呼び出しを最適化できない
                
  int productHelp(Iterator i, int accumulator) {
    while (i.hasNext()) {
      accumulator *= ((Integer)i.next()).intValue();
    }
    return accumulator;
  }

変換の後、productHelp に対して最初の呼び出しを行うと、スーパー・メソッドが呼び出されます。スーパー・メソッドは、単に、その結果を計算するためにIteratorをループします。例外はthrowされません。

このコードを2つの異なる静的コンパイラーでコンパイルして、一方の場合には生成されるコードが例外をthrowし、もう一方の場合には例外をthrowしない、ということになると非常に混乱します。


JITが変換を行うかどうか?

リスト3の例で示されているように、静的コンパイラーがJava言語のセマンティクスを維持しながらJavaコードで末尾再帰の変換を行うことは期待できません。静的コンパイルではなく、JITによる動的コンパイルが必要となります。JVMによって、JITがこれを行う場合と行わない場合があります。

使用しているJITが末尾再帰メソッドを変換するかどうかは、次の小さなテスト・クラスをコンパイルして実行してみるとわかります。


リスト4. 末尾再帰を変換を行うJITかどうかの判別
                
public class TailRecursionTest {

  private static int loop(int i) {
    return loop(i);
  }

  public static void main(String[] args) {
    loop(0);
  }
}

このクラスの loop メソッドについて、しばらく考えてみましょう。このメソッドは、それ自体を可能な限り、再帰的に呼び出します。このメソッドは戻ることがなく、外部の変数にどのような影響も与えることがないため、リスト5のように本体を置き換えても、プログラムのセマンティクスが保持されます。


リスト5. 動的変換
                
public class TailRecursionTest {

  private static int loop(int i) {
    while (true) {
    }
  }

  public static void main(String[] args) {
    loop(0);
  }
}

これは、実際に、十分な機能を持つコンパイラーならば行える変換そのものです。

使用しているJITコンパイラーが末尾再帰呼び出しを反復に変換する場合は、このプログラムはいつまでも同じように実行され続けます。このプログラムに必要なメモリーは少なく、時間がたっても増えません。

一方、JITが変換を行わない場合は、プログラムはすぐにスタック・スペースを使い尽くして、スタック・オーバーフロー・エラーを報告します。

このプログラムをいくつかのJava SDKで実行したところ、驚くべき結果が得られました。SunのHotspot JVMのバージョン1.3で実行してみると、Hotspotはこの変換を行わないことが分かりました。デフォルト設定の場合、私のマシンではスタック・スペースが1秒足らずで使い尽くされてしまいました。

一方、IBMのJVMのバージョン1.3は問題なく実行されました。つまり、この方法でコードを変換するこということになります。


まとめ

私たちの書いたコードが、常に、末尾再帰呼び出しを変換するJVMで実行されることは期待できない、ということは覚えておかなければなりません。したがって、さまざまなJVMで納得できるパフォーマンスが得られるようにするには、末尾再帰的パターンを、反復のスタイルに適合するようなコードとして書くことを、常に心がける必要があります。

しかし、注意が必要です。上記の例が示すように、この方法でのコード変換は、人間の手によって行う場合にも、ソフトウェアによって行う場合にも、バグを発生させる恐れがきわめて高いからです。


参考文献

著者について

Eric Allen氏は、コーネル大学でコンピューター・サイエンスと数学の学士号を取得しています。現在は、ライス大学の博士課程の大学院生としてJavaプログラミング言語チームに加わっています。学位を終了するためにライス大学に戻るまでは、Cycorp, IncでJavaソフトウェア開発主任として勤務していました。彼は、JavaWorldで「Java Beginner」ディスカッション・フォーラムの司会者も務めています。主な研究対象は、Java言語のセマンティック・モデルと静的分析ツールの開発であり、いずれもソース・レベルとバイトコード・レベルで研究しています。Ericは、NextGenというプログラミング言語 (汎用ランタイム型によるJava言語の拡張版) のためのライスのコンパイラーの開発にも携わってきました。連絡先は、eallen@cs.rice.edu です。

不正使用の報告のヘルプ

不正使用の報告

ありがとうございます。 このエントリーは、モデレーターの注目フラグが設定されました。


不正使用の報告のヘルプ

不正使用の報告

不正使用の報告の送信に失敗しました。


developerWorks: サイン・イン


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。 プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。 お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

表示名をお選びください

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

(半角英数字で3文字以上31文字以下にする必要があります)


「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


この記事を評価する

コメント

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219672
ArticleTitle=Javaコードの診断: Javaコードのパフォーマンスを向上させる
publish-date=05012001
author1-email=
author1-email-cc=

タグ

Help
このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。

スライダーバーを使用することで、より多く(少なく)タグを表示します。

人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。

マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。

このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。