定数を利用した最適化、というのはイメージしやすいですし、実際速くなります。今回、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そのものについては、こちらのエントリを参照してください。
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な値を定数として用いて、最適化できます。
定数については、まだ試して学びたいことがいくつかあります。また、書きます。