2020/04/30

Java の演算子の処理時間

演算命令1回の処理時間は極めて短かく、タイマの精度よりも大幅に小さいです。1回の処理時間を直接測定することはできません。それを極力正確に測定することを試みました。

測定の方針

まず、タイマの精度を誤差とみなせるほど処理時間を積み上げる必要があるため、大量の反復処理を行って測定を行う必要があります。
しかし、反復処理を行うためのインクリメント演算や反復継続条件判定のための比較演算の処理時間もまた、演算命令1回の処理時間と比べて無視できないほど大きく、これも適切に除外して測定する必要があります。
そこで、演算命令のない大量反復処理と、演算命令を含む大量反復処理を構築し、その処理時間の差から演算命令のみの処理時間を抽出するという作戦で臨みます。

具体的な手法

次のソースコードを見てください。

public class IntegerOperatorSpeedChecker {

  private static final int WARMUP = 1000;
  private static final long LOOP = 1000000;
  private static final int A = 119;
  private static final long AL = 119L;

  public static void main(String[] args) {
    for (int tries = 0; tries < 10; tries++) {
      Result result = null;

      for (int i = 0; i < WARMUP; i++) {
        result = nop(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = plus(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = minus(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = multiply(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = divide(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = mod(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = nopL(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = plusL(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = minusL(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = multiplyL(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = divideL(LOOP);
      }
      System.out.println(result);

      for (int i = 0; i < WARMUP; i++) {
        result = modL(LOOP);
      }
      System.out.println(result);
    }
  }

  private static int dummy(int v) {
    return v;
  }

  private static int dummy(int v, int w) {
    return v;
  }

  private static long dummy(long v) {
    return v;
  }

  private static long dummy(long v, long w) {
    return v;
  }

  private static Result nop(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s, 17);
    }
    return new Result("int,nop", System.nanoTime() - ns);
  }

  private static Result plus(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s + 17);
    }
    return new Result("int,+", System.nanoTime() - ns);
  }

  private static Result minus(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s - 17);
    }
    return new Result("int,-", System.nanoTime() - ns);
  }

  private static Result multiply(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s * 17);
    }
    return new Result("int,*", System.nanoTime() - ns);
  }

  private static Result divide(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s / 17);
    }
    return new Result("int,/", System.nanoTime() - ns);
  }

  private static Result mod(long loop) {
    int s = A;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      s = dummy(s % 17);
    }
    return new Result("int,%", System.nanoTime() - ns);
  }

  private static Result nopL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl, 17L);
    }
    return new Result("long,nop", System.nanoTime() - ns);
  }

  private static Result plusL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl + 17L);
    }
    return new Result("long,+", System.nanoTime() - ns);
  }

  private static Result minusL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl - 17L);
    }
    return new Result("long,-", System.nanoTime() - ns);
  }

  private static Result multiplyL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl * 17L);
    }
    return new Result("long,*", System.nanoTime() - ns);
  }

  private static Result divideL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl / 17L);
    }
    return new Result("long,/", System.nanoTime() - ns);
  }

  private static Result modL(long loop) {
    long sl = AL;
    long ns = System.nanoTime();
    for (long i = 0; i < loop; i++) {
      sl = dummy(sl % 17L);
    }
    return new Result("long,%", System.nanoTime() - ns);
  }
}

class Result {
  String op;
  long ns;

  Result(String op, long ns) {
    this.op = op;
    this.ns = ns;
  }

  @Override
  public String toString() {
    return op + "," + ns;
  }
}

演算命令の無い大量反復処理を nop()、加算命令を含む大量反復処理を plus() というメソッドに定義しています。nop() の94行目と、plus() の103行目がポイントです。nop() では dummy(s, 17) としていて、plus() では dummy(s + 17) としています。
nop() で使っている dummy(int, int) と、plus() で使っている dummy(int) という2つのメソッドは、nop() と plus() の処理時間の差を加算命令 (iadd) のみにするためのトリックになっています。

nop() と plus() のバイトコードを見てみましょう。

private static Result nop(long loop);private static Result plus(long loop);
0 bipush 1190 bipush 119
2 istore_2 [s]2 istore_2 [s]
3 invokestatic java.lang.System.nanoTime() : long [94]3 invokestatic java.lang.System.nanoTime() : long [94]
6 lstore_3 [ns]6 lstore_3 [ns]
7 lconst_07 lconst_0
8 lstore 5 [i]8 lstore 5 [i]
10 goto 2610 goto 27
13 iload_2 [s]13 iload_2 [s]
14 bipush 1714 bipush 17
16 iadd
16 invokestatic IntegerOperatorSpeedChecker.dummy(int, int) : int [98]17 invokestatic IntegerOperatorSpeedChecker.dummy(int) : int [108]
19 istore_2 [s]20 istore_2 [s]
20 lload 5 [i]21 lload 5 [i]
22 lconst_123 lconst_1
23 ladd24 ladd
24 lstore 5 [i]25 lstore 5 [i]
26 lload 5 [i]27 lload 5 [i]
28 lload_0 [loop]29 lload_0 [loop]
29 lcmp30 lcmp
30 iflt 1331 iflt 13
33 new Result [85]34 new Result [85]
36 dup37 dup
37 ldc <String "int,nop"> [100]38 ldc <String "int,+"> [110]
39 invokestatic java.lang.System.nanoTime() : long [94]40 invokestatic java.lang.System.nanoTime() : long [94]
42 lload_3 [ns]43 lload_3 [ns]
43 lsub44 lsub
44 invokespecial Result(java.lang.String, long) [102]45 invokespecial Result(java.lang.String, long) [102]
47 areturn48 areturn

これを見ると分かる通り、nop() と plus() の差は iadd 命令のみとなっています。これは、メソッド呼び出し (invokestatic) の引数の処理と、演算子のオペランドの処理が、いずれもスタック上で行われることによるものです。

  • dummy(s, 17) の処理
    • ローカル変数 s の値をスタックに読み出す
    • 17 をスタックに積む
    • invokestatic で dummy(int, int) を呼ぶ
  • dummy(s + 17) の処理
    • ローカル変数 s の値をスタックに読み出す
    • 17 をスタックに積む
    • 加算を実行する (スタックの上2つの値を加算して、答えをスタックに積む)
    • invokestatic で dummy(int) を呼ぶ

Java では、メソッド呼び出しの引数も、加算命令のオペランドも、スタックに積まれます。2つの引数のメソッドを呼ぶ場合も、2つの値を加算する場合も、2つの値をスタックに積むところまでは共通です。その後呼び出す dummy() の引数の数が異なっていますが、最終的に s も 17 もスタックから消化されることは共通しています。
なお dummy(int, int) と dummy(int) のバイトコードはいずれも以下の通り同じ内容です。
    0  iload_0 [v]
    1  ireturn

このように、plus() と nop() の処理内容の差異は、iadd 命令のみとなりました。これで、plus() の処理時間から nop() の処理時間を差し引くことにより、加算命令の処理時間を求めることができるわけです。

測定結果

上記の手法で int, long の +, -, *, /, % の処理時間を測定してみました。上記のプログラムのように、100万回の実行を10回繰り返して測定しました。結果は1回当たりに換算しました。

測定環境

CPU: 2.2 GHz クアッドコアIntel Core i7
JRE: OpenJDK Runtime Environment AdoptOpenJDK (build 14+36)

int型


+0.0995971ns
-0.1370158ns
*0.2937495ns
/1.8545765ns
%2.9148042ns
※ nop0.3521969ns

long型

+0.0921201ns
-0.0765390ns
*0.2964066ns
/1.5846781ns
%2.5261479ns
※ nop0.3684208ns

考察

int 型の結果を見ると、nop の処理時間が + の処理時間の 3.5 倍もあります。除外していないと、まったく違う結果になっていたことが分かります。
例えば、nop の処理時間を除外せずに加算と除算を比べると、4.9 倍しかないように見えてしまいます。でも実際は上記のように 18.6 倍の差がついています。

CPU も OS も VM も64ビットという時代だからでしょうか、int と long では目立った差はありません。やや long の方が速いんじゃないかと思うくらいです。昔からプログラムを書いている私は long 型を使うことにやや抵抗がありますが、時代は変わったんですね。