2011/12/06

文字列操作の速度を測ってみた(やっつけ2)

Java1.4.2~Java7.0で文字列操作の速度を測ってみた(やっつけ)を見て気になったので、こちらでも少し測定してみました。

元の記事のものに少しだけ手を加えたプログラムを用意しました。

package b;

import java.net.URLDecoder;

public class A {
 private static final String URL = "http://www.google.co.jp/?q=URLEncoder%E3%81%AF%E3%81%A9%E3%82%8C%E3%81%8F%E3%82%89%E3%81%84%E9%81%85%E3%81%84%E3%81%AE%E3%81%8B%E3%80%81commons-codec%E3%81%A8%E6%AF%94%E8%BC%83%E3%81%97%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%99";
 private static final int TIMES = 1000000;

 public static void main(String[] args) throws Throwable {
  measure();
  measure();
 }

 public static void measure() throws Throwable {
  final String input = URLDecoder.decode(URL, "UTF-8");

  final long startTime = System.currentTimeMillis();
  for (int i=0; i<TIMES; i++) {
   java.net.URLEncoder.encode(input, "UTF8");

  }
  final long endTime = System.currentTimeMillis();

  System.out.println(endTime - startTime);
 }
}

次のようなバッチファイルで測定しました。

SET MYOPTS=-Xint -client
D:\jdk4\bin\java -cp bin %MYOPTS% b.A
D:\jdk5\bin\java -cp bin %MYOPTS% b.A
D:\jdk6\bin\java -cp bin %MYOPTS% b.A
D:\jdk7\bin\java -cp bin %MYOPTS% b.A

SET MYOPTS=-Xint -server
D:\jdk4\bin\java -cp bin %MYOPTS% b.A
D:\jdk5\bin\java -cp bin %MYOPTS% b.A
D:\jdk6\bin\java -cp bin %MYOPTS% b.A
D:\jdk7\bin\java -cp bin %MYOPTS% b.A

SET MYOPTS=-client
D:\jdk4\bin\java -cp bin %MYOPTS% b.A
D:\jdk5\bin\java -cp bin %MYOPTS% b.A
D:\jdk6\bin\java -cp bin %MYOPTS% b.A
D:\jdk7\bin\java -cp bin %MYOPTS% b.A

SET MYOPTS=-server
D:\jdk4\bin\java -cp bin %MYOPTS% b.A
D:\jdk5\bin\java -cp bin %MYOPTS% b.A
D:\jdk6\bin\java -cp bin %MYOPTS% b.A
D:\jdk7\bin\java -cp bin %MYOPTS% b.A

測定結果を次に示します(2回目の測定値のみ)。

Java SEJITなしJITあり
Hotspot ClientVMHotspot ServerVMHotspot ClientVMHotspot ServerVM
1.4.21069671140951571612396
5.0 1340801402901589811667
6 13450212911069874280
7 12577211275560133483

Java SE 5.0とJava SE 6のJIT (Just In Time)コンパイラの性能差が目立ちます。
また反復処理が多い場合は、Java Hotspot ServerVMの方が良さそうですね。

2011/12/04

InterruptedExceptionは何のためにある?

InterruptedExceptionがスローされるメソッドは結構ありますが、
停止状態(ブロック状態)にあるスレッドを再開するためにInterruptedExceptionをスローするというのが基本的な考え方であって、スレッドをinterruptするのではありません
言い換えると、InterruptedExceptionは、停止状態(ブロック状態)にある処理を一度キャンセルしてスレッドの処理を再開させるためにあっても、必ずしもブロックの原因となった処理を繰り返し実行してはいけないというわけではないと考えています。

次のようなプログラムを考えてみます。
Queueから要素を取り出すスレッド(11行目で非デーモンに設定)が起動されます。このスレッドは非デーモンなので、mainスレッドが終了してもアプリケーションは即座に終了しないようになっています。
一方、mainスレッドではQueueに10個の要素を挿入し、11個目の要素の挿入で処理を終えるように指示しています。
なお実験的に20行目のコメントを外して、InterruptedExceptionをスローさせることができます。
package queue;

import java.util.concurrent.LinkedBlockingQueue;

public final class A {
 private static final LinkedBlockingQueue<Item> queue = new LinkedBlockingQueue<Item>();

 public static void main(String[] args) {
  //QueueからItemを取りだすスレッドを起動
  final Thread thread = new Thread(new Service());
  thread.setDaemon(false);
  thread.start();

  //QueueにItemを入れる
  for(int i=0; i<10; i++){
   queue.add(new Item(false, "Msg"+String.valueOf(i)));
  }

  //BlockingQueue#takeでInterruptedExceptionをスローさせる
//  thread.interrupt();

  //最後のItemを入れる
  queue.add(new Item(true, null));
 }

 //Item
 static class Item {
  private final boolean end; //trueなら最後のItemであることを示す
  private final String msg;

  Item(boolean end, String msg){
   this.end = end;
   this.msg = msg;
  }

  boolean isEnd(){
   return this.end;
  }

  String getMsg(){
   return this.msg;
  }
 }

 //QueueからItemを取りだすスレッド
 static class Service implements Runnable {
  public void run() {
   boolean roop = true;
   while(roop){
    try {
     final Item item = queue.take(); //throws InterruptedException
     if(item.isEnd()==true){
      roop = false;    //ループを抜ける
     }else{
      System.out.println(item.getMsg());
     }
    } catch (InterruptedException e) {
     System.out.println("InterruptedException was thrown.");
//     roop = false;
    }
   }
  }
 }
}
このスレッドでQueueにある要素をすべて処理しなければならないと考えるならば、InterruptedExceptionを無視して処理を継続します。たとえば、LoggingのようにすべてのLogを吐き出したいときにはInterruptedExceptionを無視することを選択するでしょう。
逆に、すべての要素を処理する必要がないスレッドの場合は、InterruptedExceptionをキャッチして、次の処理に進めるか、あるいはスレッドを即時に終了することを選択することもできます。上記プログラムの場合、59行目のコメントを外せば、whileループを抜けて次の処理に進むことになります。
InterruptedExceptionのスローによって再開されたスレッドにおいて、どのように対応するかはユーザが自由に決めてもいいわけです。予期しないInterruptionなら無視し(NOP:NO Operationとする)、意図したInterruptionなら次の処理に進めることもできます。
一口で言ってしまうとケースバイケースだと考えています。ただ、対象のスレッドがどの処理を実行中であるのかを把握できていないとThread#interruptが効果的に働かないケースが多いのではないかと思うのです。また予期しないInterruptedExceptionがスローされる可能性もないわけではありません。

2011/09/23

"Strings in switch" in Java 7 (JSR 334)

Java 7で導入されたJSR 334には、「Strings in switch」がありますが、switch構文でString型も扱えるようになっています。それがどのようにコンパイルされるのかを調べてみました。



package a;
public class E {
  public static int a(String s){
    switch(s){
    case "a":
      return 1;
    case "b":
      return 2;
    default:
      return 3;
    }
  }
}



上記ソースをjavacでコンパイルして、javap -verboseしてみました。


Classfile /E:/tmp/java/java7/bin/a/E.class
  Last modified 2011/09/23; size 491 bytes
  MD5 checksum 525f1ebe51742f599543582cb206c23c
  Compiled from "E.java"
public class a.E
  SourceFile: "E.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:
   #1 = Methodref          #7.#18         //  java/lang/Object."<init>":()V
   #2 = Methodref          #19.#20        //  java/lang/String.hashCode:()I
   #3 = String             #12            //  a
   #4 = Methodref          #19.#21        //  java/lang/String.equals:(Ljava/lang/Object;)Z
   #5 = String             #22            //  b
   #6 = Class              #23            //  a/E
   #7 = Class              #24            //  java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               a
  #13 = Utf8               (Ljava/lang/String;)I
  #14 = Utf8               StackMapTable
  #15 = Class              #25            //  java/lang/String
  #16 = Utf8               SourceFile
  #17 = Utf8               E.java
  #18 = NameAndType        #8:#9          //  "<init>":()V
  #19 = Class              #25            //  java/lang/String
  #20 = NameAndType        #26:#27        //  hashCode:()I
  #21 = NameAndType        #28:#29        //  equals:(Ljava/lang/Object;)Z
  #22 = Utf8               b
  #23 = Utf8               a/E
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/String
  #26 = Utf8               hashCode
  #27 = Utf8               ()I
  #28 = Utf8               equals
  #29 = Utf8               (Ljava/lang/Object;)Z
{
  public a.E();
    flags: ACC_PUBLIC

    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 3: 0

  public static int a(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC

    Code:
      stack=2, locals=3, args_size=1
         0: aload_0       
         1: astore_1      
         2: iconst_m1     
         3: istore_2      
         4: aload_1       
         5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
         8: lookupswitch  { // 2

                      97: 36

                      98: 50
                 default: 61
            }
        36: aload_1       
        37: ldc           #3                  // String a
        39: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        42: ifeq          61
        45: iconst_0      
        46: istore_2      
        47: goto          61
        50: aload_1       
        51: ldc           #5                  // String b
        53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        56: ifeq          61
        59: iconst_1      
        60: istore_2      
        61: iload_2       
        62: lookupswitch  { // 2

                       0: 88

                       1: 90
                 default: 92
            }
        88: iconst_1      
        89: ireturn       
        90: iconst_2      
        91: ireturn       
        92: iconst_3      
        93: ireturn       
      LineNumberTable:
        line 5: 0
        line 7: 88
        line 9: 90
        line 11: 92
      StackMapTable: number_of_entries = 6
           frame_type = 253 /* append */
             offset_delta = 36
        locals = [ class java/lang/String, int ]
           frame_type = 13 /* same */
           frame_type = 10 /* same */
           frame_type = 26 /* same */
           frame_type = 1 /* same */
           frame_type = 1 /* same */

}



案の定ハッシュ値を使っていて、次のような流れで処理していることが判ります。

  1. ローカル変数2を-1で初期化し(60~61行目)、
  2. 引数のString型のハッシュ値を取得し(63行目)、
  3. 1つ目のlookupswitchでハッシュ値が同値なら、さらにString.equals()で評価し(64~84行目)、
  4. 評価結果をローカル変数2に数値を代入して(0または1)(75、76、82、83行目)、
  5. 2つ目のlookupswitchで、ローカル変数2の値に応じた処理を行う(84~97行目)

2つのlookupswitchを使って条件分岐しているのは意外でした。