性能を考慮したJavaのプログラミング

 同じ結果を提供するプログラムであっても、プログラミング上の実現方法は一通りではありません。
プログラマは、それらの中から最適な方法を選択すべく考慮しなければなりません。
そのときのひとつの判断基準に「性能(処理速度,消費資源)」があります。
ここでは、Javaのプログラミングにおいて「性能」を意識したときに、いくつかの考慮すべき事項について、書籍「Javaの鉄則」から参考になると思ったものを紹介したいと思います。


Contents

  1. オブジェクトの作成のコスト
    1. 出来るだけ継承の少ないクラスを使う,作る
    2. オブジェクト生成時の処理を軽くする
    3. オブジェクトは必要なときに作成する
    4. そのオブジェクトが再利用可能な場合は再利用する
  2. 変数の処理性能
    1. 頻繁にアクセスする変数にはスタック変数を使用する
    2. 変数は出来るだけプリミティブ型を使う
    3. インスタンス変数の無駄な初期化はしない
  3. Javaコンパイラ,JITによるオプティマイズ
    1. メソッドを static, final, private型にしてインライン化する
    2. 定数を変数で指定する場合は final 指定する
  4. その他
    1. 文字列の連結は String よりも StringBuffer を使う
    2. Vector の検索は getメソッド を使った for ループ が速い
    3. Vector や ArrayList よりも 配列 を使う
    4. 配列のコピーには System.arraycopy を使う
    5. 共通部分式は変数に置き換える
    6. ループを展開する


  1. オブジェクトの作成のコスト

     オブジェクト(インスタンス)を作成するには大きなコスト(処理時間,メモリ消費)がかかります。
    従って、オブジェクトの作成は最小限にとどめることがプログラムの性能向上に大きく寄与します。


    1. 出来るだけ継承の少ないクラスを使う,作る

       派生クラスのオブジェクトを作成する場合、その上位クラスのコンストラクタを最上位クラスである java.lang.Object に達するまで順々に呼出します。
      コンストラクタが呼び出されると、それぞれのクラスのインスタンス変数の初期化が行われ、そしてコンストラクタ本文が実行されます。
      オブジェクトを作成するときは java.lang.Object から順に最下位クラスまでこの処理が実行されることになります。
      従って、継承の多いクラスよりも継承の少ないクラスの方がオブジェクト作成のコストは少なくて済みます。


    2. オブジェクト生成時の処理を軽くする

       前述のようにオブジェクトの生成には大きなコストがかかります。
      そこで、

      • コンストラクタの処理を必要最小限にする。
      • 初期化するオブジェクトを必要最小限にする。
      • インスタンス変数を必要最小限にする。
      • インスタンス変数の無駄な初期化をしない。(後述)

      など考慮して、オブジェクト生成時のコストを少しでも小さくすることは性能の面で大変有効です。


    3. オブジェクトは必要なときに作成する

       C言語でのプログラミング経験のあるプログラマの場合、C言語のときの癖で、メソッドの先頭ですべての変数の宣言をするコードを書いてしまうことがあります。
      結果的に宣言した変数の全てが使われるのであれば問題はありませんが、処理の流れによっては使われない場合があるのであれば、それは無駄です。
      オブジェクトは必要となったときに作成するように心がけましょう。
      何度もいうように、オブジェクトの生成には大きなコストがかかるのです。

      例:2つの文字列を連結した文字列を返すメソッド

      public String concat(String str1, String str2) throws IllegalArgumentException
      {
      IllegalArgumentException ex = new IllegalArgumentException("Parameter Error !");
      String result = new String();
      if ( str1 != null && str2 != null ) {
      result = str1 + str2;
      } else {
      throw ex;
      }
      return result;
      }

      この例の場合、パラメータが不正だった場合は result は使用しませんし、正常時には ex は必要ありません。
      このコードは、以下のようにすれば無駄は生じません。

      public String concat(String str1, String str2) throws IllegalArgumentException
      {
      if ( str1 != null && str2 != null ) {
      return new String(str1 + str2);
      } else {
      throw new IllegalArgumentException("Parameter Error !");
      }
      }


    4. そのオブジェクトが再利用可能な場合は再利用する

       オブジェクト生成には大きなコストがかかります。再利用が可能な場合は再利用すべきです。


  2. 変数の処理性能


    1. 頻繁にアクセスする変数にはスタック変数を使用する

       次のコードは、スタック変数,インスタンス変数,スタティック変数に繰り返しアクセスするものです。

      class StackVars
      {
      private int instVar;
      private static int staticVar;

      // スタック変数へのアクセス
      public void stackAccess(int val)
      {
      int j = 0;
      for ( int i=0; i<val; i++ )
      j += 1;
      }

      // インスタンス変数へのアクセス
      public void instanceAccess(int val)
      {
      for ( int i=0; i<val; i++ )
      instVar += 1;
      }

      // スタティック変数へのアクセス
      public void staticAccess(int val)
      {
      for ( int i=0; i<val; i++ )
      staticVar += 1;
      }
      }

      これを実行してみると、インスタンス変数の場合とスタティック変数の場合は処理時間に差はありませんが、スタック変数の場合は3〜5倍高速です。
      従って、上記のインスタンス変数,スタティック変数に対する処理は、以下のようにした方が処理時間が短くて済みます。

      class StackVars
      {
      private int instVar;
      private static int staticVar;

      // インスタンス変数へのアクセス
      public void instanceAccess(int val)
      {
      int j = instVar;
      for ( int i=0; i<val; i++ )
      j += 1;
      instVar = j;
      }

      // スタティック変数へのアクセス
      public void staticAccess(int val)
      {
      int j = staticVar;
      for ( int i=0; i<val; i++ )
      j += 1;
      staticVar = j;
      }
      }


    2. 変数は出来るだけプリミティブ型を使う

       変数には、プリミティブ型とそれに対するラッパークラス(int に対する Integer など)があります。
      クラスを使用すると、その値にアクセスするときにオブジェクトを介すので当然遅くなります。

      次の例では、プリミティブ型を使った方(前者)が30%ほど高速です。

      // プリミティブ型を使う
      public int usePrimitive(int increment)
      {
      int i = 5;
      i = i + increment;
      return i;
      }

      // ラッパークラスを使う
      public int useObject(Integer increment)
      {
      int i = 5;
      i = i + increment.intValue();
      return i;
      }

      また、以下のように戻り値をオブジェクトにした場合は、前述のオブジェクト生成のコストがさらにかかることになります。

      // ラッパークラスを使う
      public Integer useObject(Integer increment)
      {
      int i = 5;
      i = i + increment.intValue();
      return new Integer(i);
      }

      こうなるとプリミティブ型だけ使っていてばいいように思えますが、ラッパークラスを使わなければならない場合も当然あります。
      そのひとつの例にコレクションクラスがあります。
      コレクションクラスは、プリミティブ型を扱えません。
      以下のように Vector にプリミティブ型の値を格納することはできません。

      Vector v = new Vector();
      v.add(5);

      この場合は、次のようにラッパークラスに置き換えて格納するしかありません。

      Vector v = new Vector();
      v.add(new Integer(5));

      プリミティブ型とそれに対するラッパークラスは以下の通りです。

      プリミティブ型ラッパークラス
      booleanBoolean
      charCharacter
      byteByte
      shortShort
      intInteger
      longLong
      floatFloat
      doubleDouble


    3. インスタンス変数の無駄な初期化はしない

       Javaでは、オブジェクト生成時のインスタンス変数とstatic変数の初期化(デフォルト値)は保証されています。
      それらの変数にデフォルト値と同じ値を明示的に設定するのは、2度値を設定することになり無駄です。

      次のコードでは、オブジェクト生成時に、インスタンス変数 count, done, pt, vec は、それぞれ 0, false, null, null に初期化されます。
      そして、両コードとも countdone にはもう一度同じ値を設定することになります。

      class Foo
      {
      private int count;
      private boolean done;
      private Point pt;
      private Vector vec;

      public Foo()
      {
      count = 0;
      done = false;
      pt = new Point(0, 0);
      vec = new Vector(10);
      }
      }

      class Foo
      {
      private int count = 0;
      private boolean done = false;
      private Point pt;
      private Vector vec;

      public Foo()
      {
      pt = new Point(0, 0);
      vec = new Vector(10);
      }
      }

      この場合は、以下のコードが最適といえます。

      class Foo
      {
      private int count;
      private boolean done;
      private Point pt;
      private Vector vec;

      public Foo()
      {
      pt = new Point(0, 0);
      vec = new Vector(10);
      }
      }

      各型のデフォルト値は以下の通りです。

      デフォルト値
      booleanfalse
      char'\u0000'
      byte0
      short0
      int0
      long0
      float0
      double0
      オブジェクトの参照null

      初期化されるのはインスタンス変数とstatic変数で、スタック変数は初期化されないので注意して下さい。


  3. Javaコンパイラ,JITによるオプティマイズ


    1. メソッドを static, final, private型にしてインライン化する

       static, final, private型のメソッドは、JavaコンパイラあるいはJITによってインライン化されます。
      そのメソッドが頻繁に呼ばれるのであれば、インライン化されれば劇的に高速になります。
      ただし、インライン化されるとプログラムサイズは大きくなりますから、そのメソッドの大きさを考慮することも必要です。

      次の例のような、アクセッサ(getter, setter)メソッドなどは、サイズも小さく、使用頻度も多いので、インライン化の対象としては最適のものといえます。

      class Test
      {
      private int length;

      public int getLength()
      {
      return length;
      }

      public void setLength(int val)
      {
      length = val;
      }
      }

      上記のメソッドを下記のように final型 にしてインライン化すると、3倍以上も速くなります。

      class Test
      {
      private int length;

      public final int getLength()
      {
      return length;
      }

      public final void setLength(int val)
      {
      length = val;
      }
      }

      static, final, private型にすると、当然のことながら、それらは派生クラスで変更することは出来なくなるので、何でもかんでも闇雲にインライン化すればいいというものではありません。


    2. 定数を変数で指定する場合は final 指定する

       定数を変数で指定する場合に final 指定をすると、コンパイラは定数に置き換えます。

      以下の例では、a, b を final 指定しないと c は変数として扱われますが、

      static int a = 30;
      static int b = 60;
      int c = a + b + 100;

      次のように final 指定すると定数に置き換えられます。

      static final int a = 30;
      static final int b = 60;
      int c = a + b + 100;


  4. その他


    1. 文字列の連結は String よりも StringBuffer を使う

       文字列を連結するときに、

      String str = new String("Practical ");
      str += "Java";

      とやりがちです。
      これは、StringBuffer を使って、

      StringBuffer str = new StringBuffer("Practical ");
      str.append("Java");

      とした方がかなり高速です。(このコードの場合、後者は前者より数百倍高速です。)

      Javaコンパイラは、前者の場合、StringBuffer を作成して append メソッドを使って連結し、その結果を、新たな String を作成して格納するコードを生成します。
      Java Native Code 上では、前者は5つのオブジェクトを作成しますが、後者は3つのオブジェクトを作成するだけで済みます。


    2. Vector の検索は getメソッド を使った for ループ が速い

       Java で Vector の要素を探索するには、以下の例のように Enumeration, Iterator, ListIterator, getメソッド を使う4つの方法があります。

      // Enumeration を使った探索
      public int enumVec(Vector vec)
      {
      Enumeration enum = vec.elements();
      int total = 0;
      while ( enum.hasMoreElements() )
      total += ((Integer)(enum.nextElement())).intValue();
      return total;
      }

      // Iterator を使った探索
      public int iterVec(Vector vec)
      {
      Iterator iter = vec.iterator();
      int total = 0;
      while ( iter.hasNext() )
      total += ((Integer)(iter.next())).intValue();
      return total;
      }

      // ListIterator を使った探索
      public int listIterVec(Vector vec)
      {
      ListIterator iter = vec.listIterator();
      int total = 0;
      while ( iter.hasNext() )
      total += ((Integer)(iter.next())).intValue();
      return total;
      }

      // forループ と getメソッド を使った探索
      public int forVec(Vector vec)
      {
      int size = vec.size();
      int total = 0;
      for ( int i=0; i<size; i++ )
      total += ((Integer)(vec.get(i))).intValue();
      return total;
      }

      この例のケースでは、Iterator を使った場合と ListIterator を使った場合の処理時間はほとんど同じですが、Enumeration を使った場合は12%ほど高速です。
      ところが、getメソッド と forループ を使った場合は、それらよりも29〜34%も高速になります。
      これは、Enumeration, Iterator, ListIterator を使う方式では、ループ処理内で呼び出すメソッドが1回多い( hasMoreElements(), hasNext(), hasNext() )ためです。


    3. Vector や ArrayList よりも 配列 を使う

       以下は、探索して代入するだけの特に意味のないプログラムですが、ArrayList は Vector よりも4倍高速で、さらに、配列 は ArrayList よりも11倍高速です。

      // Vector を使う
      public void iterateVector(Vector vec)
      {
      int size = vec.size();
      Object j;
      for ( int i=0; i<size; i++ )
      j = vec.get(i);
      }

      // ArrayList を使う
      public void iterateArrayList(ArrayList al)
      {
      int size = al.size();
      Object j;
      for ( int i=0; i<size; i++ )
      j = al.get(i);
      }

      // 配列 を使う
      public void iterateArray(int[] ar)
      {
      int size = ar.length;
      int j;
      for ( int i=0; i<size; i++ )
      j = ar[i];
      }

      Vector が便利で使いやすいことはわかりますが、処理速度のことを考えれば配列で事足りるのであれば配列を使うべきでしょう。


    4. 配列のコピーには System.arraycopy を使う

       配列をコピーしたいときは、素直に以下のようにするでしょう。

      // forループ を使った配列のコピー
      public void copyArray(int[] src, int[] dest)
      {
      int size = src.length;
      for ( int i=0; i<size; i++ )
      dest[i] = src[i];
      }

      ところが、この処理は次のように System.arraycopy を使った方が2倍以上も高速です。

      // System.arraycopy を使った配列のコピー
      public void copyArray(int[] src, int[] dest)
      {
      int size = src.length;
      System.arraycopy(src, 0, dest, 0, size);
      }


    5. 共通部分式は変数に置き換える

       繰り返し使用する共通部分式は、変数に置き換えることによって式の評価は1回で済むことになり、処理時間が短縮されます。

      次のコードは、

      SomeObject[] someObj = new SomeObject[N];
      someObj[i+j] = new SomeObject();
      someObj[i+j].foo(k);
      someObj[i+j].foo(k+1);
      someObj[i+j].foo(k+2);
      someObj[i+j].foo(k+3);
      someObj[i+j].foo(k+4);

      someObj[i+j]tempObj に置き換えて、

      SomeObject[] someObj = new SomeObject[N];
      someObj[i+j] = new SomeObject();
      SomeObject tempObj = someObj[i+j];
      tempObj.foo(k);
      tempObj.foo(k+1);
      tempObj.foo(k+2);
      tempObj.foo(k+3);
      tempObj.foo(k+4);

      とすることによって2倍高速になり、さらにサイズも小さくなります。

      これは、ループ内で同じ式を実行する場合も同じです。

      int a = 10;
      int b = 20;
      int[] arr = new int[val];
      for ( int i=0; i<i; i++ )
      arr[i] = a + b;

      よりも

      int a = 10;
      int b = 20;
      int c = a + b;
      int[] arr = new int[val];
      for ( int i=0; i<i; i++ )
      arr[i] = c;

      の方が高速です。


    6. ループを展開する

       ループを使わないで個々に処理すれば速くなりますが、普通これはしないでしょう。

      以下の例では、

      int[] ia = new int[4];
      ia[0] = 10;
      ia[1] = 10;
      ia[2] = 10;
      ia[3] = 10;

      は、

      int[] ia = new int[4];
      for ( int i=0; i<4; i++ )
      ia[i] = 10;

      よりも7%高速です。