Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

static finalな定数が速いのは、何がどうなって速い?

定数を利用した最適化、というのはイメージしやすいですし、実際速くなります。今回、Alekseyさんのサンプルを使いながら、まずはstatic finalな値のコンパイルコードを実際に見て、どう速いのか学びました。

JVM Anatomy Park #15: Just-In-Time Constants

JMHのベンチマークコードは、これです。1000を、static final変数、static変数、finalなインスタンス変数、普通のインスタンス変数で割り算するだけです。

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class JustInTimeConstants {

    static final long x_static_final = Long.getLong("divisor", 1000);
    static       long x_static       = Long.getLong("divisor", 1000);
    final long x_inst_final   = Long.getLong("divisor", 1000);
    long x_inst         = Long.getLong("divisor", 1000);

    @Benchmark
    public long _static_final() { return 1000 / x_static_final; }
    @Benchmark public long _static()       { return 1000 / x_static;       }
    @Benchmark public long _inst_final()   { return 1000 / x_inst_final;   }
    @Benchmark public long _inst()         { return 1000 / x_inst;         }

}

java -jar target/benchmarks.jar JustInTimeConstantsと実行します。

Benchmark                          Mode  Cnt  Score   Error  Units
JustInTimeConstants._inst          avgt   15  7.865 ± 0.054  ns/op
JustInTimeConstants._inst_final    avgt   15  7.875 ± 0.039  ns/op
JustInTimeConstants._static        avgt   15  9.409 ± 0.072  ns/op
JustInTimeConstants._static_final  avgt   15  1.795 ± 0.012  ns/op

想像通り、static finalの場合が、一番速いです。ここで、Long.getLong("divisor", 1000);としているのは、コンパイラにstatic final以外も実質的に定数であると想定させないためです。たとえば、以下のように、単に1000Lを代入するコードに変えてみます。

    static final long x_static_final = 1000L;
    static       long x_static       = 1000L;
    final long x_inst_final   = 1000L;
    long x_inst         = 1000L;

実行すると、以下のベンチマーク結果になります。

Benchmark                          Mode  Cnt  Score   Error  Units
JustInTimeConstants._inst          avgt   15  7.897 ± 0.043  ns/op
JustInTimeConstants._inst_final    avgt   15  1.776 ± 0.012  ns/op
JustInTimeConstants._static        avgt   15  9.411 ± 0.048  ns/op
JustInTimeConstants._static_final  avgt   15  1.783 ± 0.014  ns/op

finalなインスタンス変数のケースも、速くなっています。

JIT生成コードを見る

java -jar target/benchmarks.jar JustInTimeConstants -prof perfasmと、-prof perfasmをつけて、実行してみます。JMHのperfasmそのものについては、こちらのエントリを参照してください。

www.sakatakoichi.com

finalなインスタンス変数の場合

まず、finalなインスタンス変数の場合、割り算を実行しているコードは、以下のようなものです。

  0.04%  │  ↗  0x00007f3068674e60: mov    0x40(%rsp),%r10
  2.64%  │  │  0x00007f3068674e65: mov    0x10(%r10),%r10                ;*getfield x_inst_final {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.JustInTimeConstants::_inst_final@4 (line 36)
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@17 (line 190)
  0.08%  │  │  0x00007f3068674e69: test   %r10,%r10
         │  │  0x00007f3068674e6c: je     0x00007f3068674ef1
  0.17%  │  │  0x00007f3068674e72: mov    $0x3e8,%eax
  0.02%  │  │  0x00007f3068674e77: movabs $0x8000000000000000,%rdx
  2.47%  │  │  0x00007f3068674e81: cmp    %rdx,%rax
         │╭ │  0x00007f3068674e84: jne    0x00007f3068674e8e
         ││ │  0x00007f3068674e86: xor    %edx,%edx
         ││ │  0x00007f3068674e88: cmp    $0xffffffffffffffff,%r10
         ││╭│  0x00007f3068674e8c: je     0x00007f3068674e93
  0.12%  │↘││  0x00007f3068674e8e: cqto   
  0.25%  │ ││  0x00007f3068674e90: idiv   %r10                           ;*ldiv {reexecute=0 rethrow=0 return_oop=0}
         │ ││                                                            ; - com.sakatakoichi.JustInTimeConstants::_inst_final@7 (line 36)
         │ ││                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@17 (line 190)
 70.87%  │ ↘│  0x00007f3068674e93: mov    0x38(%rsp),%rsi
  0.29%  │  │  0x00007f3068674e98: mov    %rax,%rdx
         │  │  0x00007f3068674e9b: callq  0x00007f3060ba4900             ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
         │  │                                                            ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@20 (line 190)
         │  │                                                            ;   {optimized virtual_call}
  2.59%  │  │  0x00007f3068674ea0: mov    (%rsp),%r10
  0.58%  │  │  0x00007f3068674ea4: movzbl 0x94(%r10),%r11d               ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@33 (line 192)
  0.04%  │  │  0x00007f3068674eac: mov    0x108(%r15),%r10
  2.55%  │  │  0x00007f3068674eb3: add    $0x1,%rbp                      ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
         │  │                                                            ;*ifeq {reexecute=1 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@33 (line 192)
  0.10%  │  │  0x00007f3068674eb7: test   %eax,(%r10)                    ;   {poll}
  0.37%  │  │  0x00007f3068674eba: test   %r11d,%r11d
         │  ╰  0x00007f3068674ebd: je     0x00007f3068674e60             ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
         │                                                               ; - com.sakatakoichi.generated.JustInTimeConstants__inst_final_jmhTest::_inst_final_avgt_jmhStub@36 (line 193)

mov 0x10(%r10),%r10 ;*getfield x_inst_finalと、フィールドから値を取り出し、レジスタに格納します。 mov $0x3e8,%eaxで、0x3e8 = 1000です。そして、idiv %r10 ;*ldiv {reexecute=0 rethrow=0 return_oop=0}で割ります。サイクルのほとんどを、実際の割り算処理に使っています。

インスタンス変数の場合

インスタンス変数の場合は、上述のfinalなインスタンス変数の場合と、同じです。以下のようなコードです。

  0.02%  │  ↗  0x00007efc7ba0cbe0: mov    0x40(%rsp),%r10
  2.63%  │  │  0x00007efc7ba0cbe5: mov    0x18(%r10),%r10                ;*getfield x_inst {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.JustInTimeConstants::_inst@4 (line 37)
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@17 (line 190)
  0.21%  │  │  0x00007efc7ba0cbe9: test   %r10,%r10
         │  │  0x00007efc7ba0cbec: je     0x00007efc7ba0cc71
  0.29%  │  │  0x00007efc7ba0cbf2: mov    $0x3e8,%eax
         │  │  0x00007efc7ba0cbf7: movabs $0x8000000000000000,%rdx
  2.84%  │  │  0x00007efc7ba0cc01: cmp    %rdx,%rax
         │╭ │  0x00007efc7ba0cc04: jne    0x00007efc7ba0cc0e
         ││ │  0x00007efc7ba0cc06: xor    %edx,%edx
         ││ │  0x00007efc7ba0cc08: cmp    $0xffffffffffffffff,%r10
         ││╭│  0x00007efc7ba0cc0c: je     0x00007efc7ba0cc13
  0.19%  │↘││  0x00007efc7ba0cc0e: cqto   
  0.37%  │ ││  0x00007efc7ba0cc10: idiv   %r10                           ;*ldiv {reexecute=0 rethrow=0 return_oop=0}
         │ ││                                                            ; - com.sakatakoichi.JustInTimeConstants::_inst@7 (line 37)
         │ ││                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@17 (line 190)
 72.30%  │ ↘│  0x00007efc7ba0cc13: mov    0x38(%rsp),%rsi
  0.21%  │  │  0x00007efc7ba0cc18: mov    %rax,%rdx
         │  │  0x00007efc7ba0cc1b: callq  0x00007efc73f3d900             ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
         │  │                                                            ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@20 (line 190)
         │  │                                                            ;   {optimized virtual_call}
  2.86%  │  │  0x00007efc7ba0cc20: mov    (%rsp),%r10
  0.37%  │  │  0x00007efc7ba0cc24: movzbl 0x94(%r10),%r11d               ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@33 (line 192)
         │  │  0x00007efc7ba0cc2c: mov    0x108(%r15),%r10
  2.41%  │  │  0x00007efc7ba0cc33: add    $0x1,%rbp                      ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
         │  │                                                            ;*ifeq {reexecute=1 rethrow=0 return_oop=0}
         │  │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@33 (line 192)
  0.14%  │  │  0x00007efc7ba0cc37: test   %eax,(%r10)                    ;   {poll}
  0.21%  │  │  0x00007efc7ba0cc3a: test   %r11d,%r11d
         │  ╰  0x00007efc7ba0cc3d: je     0x00007efc7ba0cbe0             ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
         │                                                               ; - com.sakatakoichi.generated.JustInTimeConstants__inst_jmhTest::_inst_avgt_jmhStub@36 (line 193)

static変数の場合

static変数の場合は、当然異なる部分があります。

  2.41%    ↗  0x00007ff2ac673ef0: movabs $0x7157d2e38,%r10              ;   {oop(a 'java/lang/Class'{0x00000007157d2e38} = 'com/sakatakoichi/JustInTimeConstants')}
  0.02%    │  0x00007ff2ac673efa: mov    0x70(%r10),%r10                ;*getstatic x_static {reexecute=0 rethrow=0 return_oop=0}
           │                                                            ; - com.sakatakoichi.JustInTimeConstants::_static@3 (line 35)
           │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@17 (line 190)
  0.06%    │  0x00007ff2ac673efe: test   %r10,%r10
           │  0x00007ff2ac673f01: je     0x00007ff2ac673f8d
  0.04%    │  0x00007ff2ac673f07: mov    %r8,(%rsp)
  2.10%    │  0x00007ff2ac673f0b: mov    $0x3e8,%eax
           │  0x00007ff2ac673f10: movabs $0x8000000000000000,%rdx
           │  0x00007ff2ac673f1a: cmp    %rdx,%rax
         ╭ │  0x00007ff2ac673f1d: jne    0x00007ff2ac673f27
         │ │  0x00007ff2ac673f1f: xor    %edx,%edx
         │ │  0x00007ff2ac673f21: cmp    $0xffffffffffffffff,%r10
         │╭│  0x00007ff2ac673f25: je     0x00007ff2ac673f2c
  0.02%  ↘││  0x00007ff2ac673f27: cqto   
  2.43%   ││  0x00007ff2ac673f29: idiv   %r10                           ;*ldiv {reexecute=0 rethrow=0 return_oop=0}
          ││                                                            ; - com.sakatakoichi.JustInTimeConstants::_static@6 (line 35)
          ││                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@17 (line 190)
 73.67%   ↘│  0x00007ff2ac673f2c: mov    0x38(%rsp),%rsi
  0.02%    │  0x00007ff2ac673f31: mov    %rax,%rdx
           │  0x00007ff2ac673f34: data16 xchg %ax,%ax
  2.56%    │  0x00007ff2ac673f37: callq  0x00007ff2a4ba4900             ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
           │                                                            ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
           │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@20 (line 190)
           │                                                            ;   {optimized virtual_call}
           │  0x00007ff2ac673f3c: mov    (%rsp),%r8
  0.08%    │  0x00007ff2ac673f40: movzbl 0x94(%r8),%r11d                ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
           │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@33 (line 192)
  2.69%    │  0x00007ff2ac673f48: mov    0x108(%r15),%r10
  0.02%    │  0x00007ff2ac673f4f: add    $0x1,%rbp                      ; ImmutableOopMap{r8=Oop [48]=Oop [56]=Oop [64]=Oop }
           │                                                            ;*ifeq {reexecute=1 rethrow=0 return_oop=0}
           │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@33 (line 192)
  0.04%    │  0x00007ff2ac673f53: test   %eax,(%r10)                    ;   {poll}
           │  0x00007ff2ac673f56: test   %r11d,%r11d
           ╰  0x00007ff2ac673f59: je     0x00007ff2ac673ef0             ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                                        ; - com.sakatakoichi.generated.JustInTimeConstants__static_jmhTest::_static_avgt_jmhStub@36 (line 193)

mov 0x70(%r10),%r10 ;*getstatic x_static {reexecute=0 rethrow=0 return_oop=0}でstaticな値を取り出します。このコードの前に、movabs $0x7157d2e38,%r10 ; {oop(a 'java/lang/Class'{0x00000007157d2e38} = 'com/sakatakoichi/JustInTimeConstants')}でネイティブなクラスオブジェクトを用意して、static変数を読み取れるようにしています。

static final変数の場合

最後に、static final変数の場合です。

  2.19%  ↗  0x00007fa32424f250: mov    %r8,(%rsp)
  8.22%  │  0x00007fa32424f254: mov    0x38(%rsp),%rsi
  1.23%  │  0x00007fa32424f259: mov    0x8(%rsp),%rdx
         │  0x00007fa32424f25e: nop
  2.36%  │  0x00007fa32424f25f: callq  0x00007fa31c77f900             ; ImmutableOopMap{[48]=Oop [56]=Oop [64]=Oop [0]=Oop }
         │                                                            ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
         │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_final_jmhTest::_static_final_avgt_jmhStub@20 (line 190)
         │                                                            ;   {optimized virtual_call}
 12.63%  │  0x00007fa32424f264: mov    (%rsp),%r8
  1.34%  │  0x00007fa32424f268: movzbl 0x94(%r8),%r11d                ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
         │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_final_jmhTest::_static_final_avgt_jmhStub@33 (line 192)
  2.34%  │  0x00007fa32424f270: mov    0x108(%r15),%r10
  9.87%  │  0x00007fa32424f277: add    $0x1,%rbp                      ; ImmutableOopMap{r8=Oop [48]=Oop [56]=Oop [64]=Oop }
         │                                                            ;*ifeq {reexecute=1 rethrow=0 return_oop=0}
         │                                                            ; - com.sakatakoichi.generated.JustInTimeConstants__static_final_jmhTest::_static_final_avgt_jmhStub@33 (line 192)
  1.02%  │  0x00007fa32424f27b: test   %eax,(%r10)                    ;   {poll}
  0.04%  │  0x00007fa32424f27e: test   %r11d,%r11d
         ╰  0x00007fa32424f281: je     0x00007fa32424f250             ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                                      ; - com.sakatakoichi.generated.JustInTimeConstants__static_final_jmhTest::_static_final_avgt_jmhStub@36 (line 193)

mov 0x8(%rsp),%rdxだけで終わっています。static finalな変数の値が1000であり、1000 / 1000の計算結果である1を、保持しているからです。実際の計算処理がないため、速くなっているわけです。

まとめ

Javaコンパイラ(javac)は、static finalな変数を最適化することはできません。static finalな変数は、実行時の値で初期化されるものであるため、単に数値を代入しているとしても、コンパイラには何もできません。

JITコンパイル時には、クラスの初期化は終わっているので、static finalな値を定数として用いて、最適化できます。

定数については、まだ試して学びたいことがいくつかあります。また、書きます。