Метод JVM JIT пересчитывается для чистых методов

Сравнительный анализ следующего кода Java с использованием jmh:

interface MyInterface {
    public int test(int i);
}

class A implements MyInterface {
    public int test(int i) {
        return (int)Math.sin(Math.cos(i));
    }
}

@State(Scope.Thread)
public class MyBenchmark {
    public MyInterface inter;

    @Setup(Level.Trial)
    public void init() {
        inter = new A();
    }

    @Benchmark
    public void testMethod(Blackhole sink) {
        int[] res = new int[2];
        res[0] = inter.test(1);
        res[1] = inter.test(1);
        sink.consume(res);
    }
}

Используя mvn package && java -XX:-UseCompressedOops -XX:CompileCommand='print, *.testMethod' -jar target/benchmarks.jar -wi 10 -i 1 -f 1, я смог получить сборку, и если мы сосредоточимся на сборке из C2 (как показано ниже), мы увидим, что и cos, и sin вызываются дважды.

ImmutableOopMap{}pc offsets: 796 812 828 Compiled method (c2)     402  563       4       org.sample.MyBenchmark::testMethod (42 bytes)
 total in heap  [0x00007efd3d74fb90,0x00007efd3d7503a0] = 2064
 relocation     [0x00007efd3d74fcd0,0x00007efd3d74fd08] = 56
 constants      [0x00007efd3d74fd20,0x00007efd3d74fd40] = 32
 main code      [0x00007efd3d74fd40,0x00007efd3d750040] = 768
 stub code      [0x00007efd3d750040,0x00007efd3d750068] = 40
 oops           [0x00007efd3d750068,0x00007efd3d750070] = 8
 metadata       [0x00007efd3d750070,0x00007efd3d750080] = 16
 scopes data    [0x00007efd3d750080,0x00007efd3d750108] = 136
 scopes pcs     [0x00007efd3d750108,0x00007efd3d750358] = 592
 dependencies   [0x00007efd3d750358,0x00007efd3d750360] = 8
 handler table  [0x00007efd3d750360,0x00007efd3d750390] = 48
 nul chk table  [0x00007efd3d750390,0x00007efd3d7503a0] = 16
----------------------------------------------------------------------
org/sample/MyBenchmark.testMethod(Lorg/openjdk/jmh/infra/Blackhole;)V  [0x00007efd3d74fd40, 0x00007efd3d750068]  808 bytes
[Constants]
  0x00007efd3d74fd20 (offset:    0): 0x00000000   0x3ff0000000000000
  0x00007efd3d74fd24 (offset:    4): 0x3ff00000
  0x00007efd3d74fd28 (offset:    8): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007efd3d74fd2c (offset:   12): 0xf4f4f4f4
  0x00007efd3d74fd30 (offset:   16): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007efd3d74fd34 (offset:   20): 0xf4f4f4f4
  0x00007efd3d74fd38 (offset:   24): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007efd3d74fd3c (offset:   28): 0xf4f4f4f4
Argument 0 is unknown.RIP: 0x7efd3d74fd40 Code size: 0x00000328
[Entry Point]
  # {method} {0x00007efd35857f08} 'testMethod' '(Lorg/openjdk/jmh/infra/Blackhole;)V' in 'org/sample/MyBenchmark'
  # this:     rsi:rsi   = 'org/sample/MyBenchmark'
  # parm0:    rdx:rdx   = 'org/openjdk/jmh/infra/Blackhole'
  #           [sp+0x30]  (sp of caller)
  0x00007efd3d74fd40: cmp     0x8(%rsi),%rax    ;   {no_reloc}
  0x00007efd3d74fd44: jne     0x7efd35c99c60    ;   {runtime_call ic_miss_stub}
  0x00007efd3d74fd4a: nop
  0x00007efd3d74fd4c: nopl    0x0(%rax)
[Verified Entry Point]
  0x00007efd3d74fd50: mov     %eax,0xfffffffffffec000(%rsp)
  0x00007efd3d74fd57: push    %rbp
  0x00007efd3d74fd58: sub     $0x20,%rsp        ;*synchronization entry
                                                ; - org.sample.MyBenchmark::testMethod@-1 (line 64)

  0x00007efd3d74fd5c: mov     %rdx,(%rsp)
  0x00007efd3d74fd60: mov     %rsi,%rbp
  0x00007efd3d74fd63: mov     0x60(%r15),%rbx
  0x00007efd3d74fd67: mov     %rbx,%r10
  0x00007efd3d74fd6a: add     $0x1a8,%r10
  0x00007efd3d74fd71: cmp     0x70(%r15),%r10
  0x00007efd3d74fd75: jnb     0x7efd3d74ffcc
  0x00007efd3d74fd7b: mov     %r10,0x60(%r15)
  0x00007efd3d74fd7f: prefetchnta 0xc0(%r10)
  0x00007efd3d74fd87: movq    $0x1,(%rbx)
  0x00007efd3d74fd8e: prefetchnta 0x100(%r10)
  0x00007efd3d74fd96: mov     %rbx,%rdi
  0x00007efd3d74fd99: add     $0x18,%rdi
  0x00007efd3d74fd9d: prefetchnta 0x140(%r10)
  0x00007efd3d74fda5: prefetchnta 0x180(%r10)
  0x00007efd3d74fdad: movabs  $0x7efd350d9b38,%r10  ;   {metadata({type array int})}
  0x00007efd3d74fdb7: mov     %r10,0x8(%rbx)
  0x00007efd3d74fdbb: movl    $0x64,0x10(%rbx)
  0x00007efd3d74fdc2: mov     $0x32,%ecx
  0x00007efd3d74fdc7: xor     %rax,%rax
  0x00007efd3d74fdca: shl     $0x3,%rcx
  0x00007efd3d74fdce: rep stosb (%rdi)          ;*newarray {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@4 (line 65)

  0x00007efd3d74fdd1: mov     0x10(%rbp),%r10   ;*getfield inter {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@20 (line 67)

  0x00007efd3d74fdd5: mov     0x8(%r10),%r10    ; implicit exception: dispatches to 0x00007efd3d74fffd
  0x00007efd3d74fdd9: movabs  $0x7efd3587f8c8,%r11  ;   {metadata('org/sample/A')}
  0x00007efd3d74fde3: cmp     %r11,%r10
  0x00007efd3d74fde6: jne     0x7efd3d74fffd    ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fdec: vmovsd  0xffffff2c(%rip),%xmm0  ;   {section_word}
  0x00007efd3d74fdf4: vmovq   %xmm0,%r13
  0x00007efd3d74fdf9: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74fe03: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe06: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74fe10: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe13: vcvttsd2si %xmm0,%r11d
  0x00007efd3d74fe17: cmp     $0x80000000,%r11d
  0x00007efd3d74fe1e: jne     0x7efd3d74fe30
  0x00007efd3d74fe20: sub     $0x8,%rsp
  0x00007efd3d74fe24: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74fe29: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74fe2e: pop     %r11
  0x00007efd3d74fe30: mov     %r11d,0x18(%rbx)  ;*iastore {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@29 (line 67)

  0x00007efd3d74fe34: mov     $0x1,%ebp
  0x00007efd3d74fe39: jmp     0x7efd3d74fe43
  0x00007efd3d74fe3b: nopl    0x0(%rax,%rax)
  0x00007efd3d74fe40: mov     %r11d,%ebp        ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe43: vmovq   %r13,%xmm0
  0x00007efd3d74fe48: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74fe52: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe55: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74fe5f: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe62: vcvttsd2si %xmm0,%r11d
  0x00007efd3d74fe66: cmp     $0x80000000,%r11d
  0x00007efd3d74fe6d: jne     0x7efd3d74fe7f
  0x00007efd3d74fe6f: sub     $0x8,%rsp
  0x00007efd3d74fe73: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74fe78: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74fe7d: pop     %r11
  0x00007efd3d74fe7f: mov     %r11d,0x18(%rbx,%rbp,4)  ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe84: vmovq   %r13,%xmm0
  0x00007efd3d74fe89: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74fe93: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fe96: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74fea0: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fea3: vcvttsd2si %xmm0,%r11d
  0x00007efd3d74fea7: cmp     $0x80000000,%r11d
  0x00007efd3d74feae: jne     0x7efd3d74fec0
  0x00007efd3d74feb0: sub     $0x8,%rsp
  0x00007efd3d74feb4: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74feb9: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74febe: pop     %r11
  0x00007efd3d74fec0: mov     %r11d,0x1c(%rbx,%rbp,4)  ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fec5: vmovq   %r13,%xmm0
  0x00007efd3d74feca: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74fed4: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fed7: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74fee1: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74fee4: vcvttsd2si %xmm0,%r11d
  0x00007efd3d74fee8: cmp     $0x80000000,%r11d
  0x00007efd3d74feef: jne     0x7efd3d74ff01
  0x00007efd3d74fef1: sub     $0x8,%rsp
  0x00007efd3d74fef5: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74fefa: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74feff: pop     %r11
  0x00007efd3d74ff01: mov     %r11d,0x20(%rbx,%rbp,4)  ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff06: vmovq   %r13,%xmm0
  0x00007efd3d74ff0b: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74ff15: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff18: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74ff22: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff25: vcvttsd2si %xmm0,%r11d
  0x00007efd3d74ff29: cmp     $0x80000000,%r11d
  0x00007efd3d74ff30: jne     0x7efd3d74ff42
  0x00007efd3d74ff32: sub     $0x8,%rsp
  0x00007efd3d74ff36: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74ff3b: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74ff40: pop     %r11
  0x00007efd3d74ff42: mov     %r11d,0x24(%rbx,%rbp,4)  ;*iastore {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@29 (line 67)

  0x00007efd3d74ff47: mov     %ebp,%r11d
  0x00007efd3d74ff4a: add     $0x4,%r11d        ;*iinc {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@30 (line 66)

  0x00007efd3d74ff4e: cmp     $0x61,%r11d
  0x00007efd3d74ff52: jl      0x7efd3d74fe40    ;*if_icmpge {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@13 (line 66)

  0x00007efd3d74ff58: cmp     $0x64,%r11d
  0x00007efd3d74ff5c: jnl     0x7efd3d74ffac
  0x00007efd3d74ff5e: add     $0x4,%ebp         ;*iinc {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@30 (line 66)

  0x00007efd3d74ff61: nop                       ;*synchronization entry
                                                ; - org.sample.A::test@-1 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff64: vmovq   %r13,%xmm0
  0x00007efd3d74ff69: movabs  $0x7efd35c53b33,%r10
  0x00007efd3d74ff73: callq   %r10              ;*invokestatic cos {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@2 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff76: movabs  $0x7efd35c5349c,%r10
  0x00007efd3d74ff80: callq   %r10              ;*invokestatic sin {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.A::test@5 (line 49)
                                                ; - org.sample.MyBenchmark::testMethod@24 (line 67)

  0x00007efd3d74ff83: vcvttsd2si %xmm0,%r10d
  0x00007efd3d74ff87: cmp     $0x80000000,%r10d
  0x00007efd3d74ff8e: jne     0x7efd3d74ffa0
  0x00007efd3d74ff90: sub     $0x8,%rsp
  0x00007efd3d74ff94: vmovsd  %xmm0,(%rsp)
  0x00007efd3d74ff99: callq   0x7efd35ca745b    ;   {runtime_call StubRoutines (2)}
  0x00007efd3d74ff9e: pop     %r10
  0x00007efd3d74ffa0: mov     %r10d,0x18(%rbx,%rbp,4)  ;*iastore {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@29 (line 67)

  0x00007efd3d74ffa5: incl    %ebp              ;*iinc {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@30 (line 66)

  0x00007efd3d74ffa7: cmp     $0x64,%ebp
  0x00007efd3d74ffaa: jl      0x7efd3d74ff64
  0x00007efd3d74ffac: mov     (%rsp),%rsi
  0x00007efd3d74ffb0: test    %rsi,%rsi
  0x00007efd3d74ffb3: je      0x7efd3d74ffe8    ;*if_icmpge {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@13 (line 66)

  0x00007efd3d74ffb5: mov     %rbx,%rdx
  0x00007efd3d74ffb8: nop
  0x00007efd3d74ffbb: callq   0x7efd362c50e0    ; ImmutableOopMap{}
                                                ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@38 (line 69)
                                                ;   {optimized virtual_call}
  0x00007efd3d74ffc0: add     $0x20,%rsp
  0x00007efd3d74ffc4: pop     %rbp
  0x00007efd3d74ffc5: test    %eax,0x18f98035(%rip)  ;   {poll_return}
  0x00007efd3d74ffcb: retq
  0x00007efd3d74ffcc: mov     $0x64,%edx
  0x00007efd3d74ffd1: movabs  $0x7efd350d9b38,%rsi  ;   {metadata({type array int})}
  0x00007efd3d74ffdb: callq   0x7efd35d5fd60    ; ImmutableOopMap{rbp=Oop [0]=Oop }
                                                ;*newarray {reexecute=0 rethrow=0 return_oop=1}
                                                ; - org.sample.MyBenchmark::testMethod@4 (line 65)
                                                ;   {runtime_call _new_array_Java}
  0x00007efd3d74ffe0: mov     %rax,%rbx
  0x00007efd3d74ffe3: jmpq    0x7efd3d74fdd1
  0x00007efd3d74ffe8: mov     $0xfffffff6,%esi
  0x00007efd3d74ffed: mov     %rbx,%rbp
  0x00007efd3d74fff0: nop
  0x00007efd3d74fff3: callq   0x7efd35c9b560    ; ImmutableOopMap{rbp=Oop }
                                                ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@38 (line 69)
                                                ;   {runtime_call UncommonTrapBlob}
  0x00007efd3d74fff8: callq   0x7efd55167aa0    ;   {runtime_call}
  0x00007efd3d74fffd: mov     $0xffffff86,%esi
  0x00007efd3d750002: mov     %rbx,0x8(%rsp)
  0x00007efd3d750007: callq   0x7efd35c9b560    ; ImmutableOopMap{rbp=Oop [0]=Oop [8]=Oop }
                                                ;*aload_3 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@16 (line 67)
                                                ;   {runtime_call UncommonTrapBlob}
  0x00007efd3d75000c: callq   0x7efd55167aa0    ;*newarray {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@4 (line 65)
                                                ;   {runtime_call}
  0x00007efd3d750011: mov     %rax,%rsi
  0x00007efd3d750014: jmp     0x7efd3d750019
  0x00007efd3d750016: mov     %rax,%rsi         ;*invokevirtual consume {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.MyBenchmark::testMethod@38 (line 69)

  0x00007efd3d750019: add     $0x20,%rsp
  0x00007efd3d75001d: pop     %rbp
  0x00007efd3d75001e: jmpq    0x7efd35d64160    ;   {runtime_call _rethrow_Java}
  0x00007efd3d750023: hlt
  0x00007efd3d750024: hlt
  0x00007efd3d750025: hlt
  0x00007efd3d750026: hlt
  0x00007efd3d750027: hlt
  0x00007efd3d750028: hlt
  0x00007efd3d750029: hlt
  0x00007efd3d75002a: hlt
  0x00007efd3d75002b: hlt
  0x00007efd3d75002c: hlt
  0x00007efd3d75002d: hlt
  0x00007efd3d75002e: hlt
  0x00007efd3d75002f: hlt
  0x00007efd3d750030: hlt
  0x00007efd3d750031: hlt
  0x00007efd3d750032: hlt
  0x00007efd3d750033: hlt
  0x00007efd3d750034: hlt
  0x00007efd3d750035: hlt
  0x00007efd3d750036: hlt
  0x00007efd3d750037: hlt
  0x00007efd3d750038: hlt
  0x00007efd3d750039: hlt
  0x00007efd3d75003a: hlt
  0x00007efd3d75003b: hlt
  0x00007efd3d75003c: hlt
  0x00007efd3d75003d: hlt
  0x00007efd3d75003e: hlt
  0x00007efd3d75003f: hlt

Я ожидал, что результат от inter.test кэшируется или что-то в этом роде, так что inter.test (sin и cos) вызывается только один раз. Любые варианты, которые я могу использовать, чтобы сделать JVM (JIT) для этого? Или что мешает JVM (JIT) увидеть, что метод чист?

ENV:

$ java -version
openjdk version "9-internal"
OpenJDK Runtime Environment (build 9-internal+0-2016-04-14-195246.buildd.src)
OpenJDK 64-Bit Server VM (build 9-internal+0-2016-04-14-195246.buildd.src, mixed mode)
# jmh version
<jmh.version>1.19</jmh.version>

person Albert Netymk    schedule 31.12.2017    source источник
comment
Мне кажется, что эти вызовы просто полностью встроены, какую версию java и jmh вы используете?   -  person Jorn Vernee    schedule 31.12.2017
comment
С Java 9 я вижу тот же код. Я не верю, что вызов можно опустить, если он не встроен, так как это может иметь побочный эффект.   -  person Jorn Vernee    schedule 31.12.2017
comment
Вы можете переписать его на C++ с применением constness, проверкой типов и другими приятными вещами, чтобы компилятор хорошо знал, что он делает. В Java вам нужно держать JVM в руках в случае производительности, но в целом у меня ужасный опыт кодирования, связанного с производительностью, с Java, ваш просто соответствует тому, что я когда-либо видел. Единственный разумный подход для меня — принять Java такой, какая она есть, простым языком программирования, легким в освоении до среднего уровня и коммерчески успешным. Не могу найти в этом ничего более хорошего, а начиная с С++ 11, С++ всегда выигрывает даже по элегантности исходного кода.   -  person Ped7g    schedule 31.12.2017
comment
@JornVernee: я думаю, что ОП надеется, что JVM знает, что Math.sin() и Math.cos() являются чистыми функциями (без побочных эффектов), поэтому она может CSE их. Конечно, если Java поддерживает немаскированные исключения FP, математические функции могут иметь побочные эффекты. Даже 1.0 вызовет исключение FP Inexact. Тем не менее, я удивлен, что Java9 не выполняет распространение констант через sin() и cos(). Может быть, запустить его дольше, пока он не будет повторно JIT с большей оптимизацией?   -  person Peter Cordes    schedule 01.01.2018
comment
Какую производительность вы получили в ns/op? Можете ли вы показать метод весь? В нескольких последних версиях JDK использует встроенные функции для Math.sin и Math.cos, которые сводятся к встроенному быстрому пути с использованием fsin и fcos и более медленному пути в зависимости от того, требуется ли сокращение аргумента и тому подобное. Таким образом, вашего отрывка недостаточно, чтобы сделать вывод, что строка call %r10 действительно выполняется.   -  person BeeOnRope    schedule 01.01.2018
comment
@ Ped7g Переписывание на C ++ дало бы сборку (производительность), которую я ожидал, но мое ограничение - использовать чистую Java.   -  person Albert Netymk    schedule 01.01.2018
comment
@PeterCordes Он уже скомпилирован C2 (последний уровень), и этот метод полностью детерминирован. Я не знаю, что может измениться, чтобы вызвать повторную JIT.   -  person Albert Netymk    schedule 01.01.2018
comment
@BeeOnRope Фактическое число ns/op, вероятно, не очень интересно, поскольку оно зависит от аппаратного обеспечения. У меня есть обновление, чтобы включить полную сборку. Даже если используется fsin, оно используется дважды. Я ожидал, что test() вызывается один раз, а результат повторно используется для второго вызова.   -  person Albert Netymk    schedule 01.01.2018
comment
@AlbertNetymk - это интересно, поскольку оно сообщает вам, идете ли вы по быстрому или медленному пути. Это вовсе не эта аппаратная зависимость, если только у вас не очень странное аппаратное обеспечение. Например, производительность fsin на оборудовании x86 оставалась примерно постоянной в течение примерно десяти лет. В любом случае, как я уже упоминал, фрагмент кода, на который вы ссылались выше, вероятно, даже не выполняется. Это медленный путь, но если вы посмотрите вокруг, вы, вероятно, найдете быстрый путь, где он просто загружает окончательный результат непосредственно из ячейки памяти (поскольку ваш ввод постоянен).   -  person BeeOnRope    schedule 01.01.2018
comment
Код не компилируется для меня в Java 8 - у него есть предварительные тесты для проверки диапазона аргументов, а затем он вызывает медленный путь (call %r10) в одном случае, но в другом испускает быстрый -path код, который вызывает встроенные fsin и fcos (будет ли это еще быстрее, подлежит обсуждению). На самом деле, в некоторых случаях, когда аргумент является константой и меньше PI / 4, он вообще не вызывает какой-либо триггерный метод, а просто загружает окончательный ответ из памяти (из пула констант, поддерживаемого JIT).   -  person BeeOnRope    schedule 01.01.2018
comment
Вы можете увидеть логику генерации JIT-кода для этих функций здесь. В частности, на x86 Matcher::strict_fp_requires_explicit_rounding кажется истинным, и он идет по этому пути, что означает, что значения ‹ Math.PI / 4 обрабатываются совсем иначе (встраиваются, быстрее), чем значения › Math.PI / 4. Вы можете убедиться в этом, попробовав, например, Math.PI / 4 - 0.01 и Math.PI / 4 + 0.01. Для меня разница в 5 раз.   -  person BeeOnRope    schedule 01.01.2018
comment
@BeeOnRope Спасибо за разработку. Я мог воспроизвести вывод fsin, используя oracle jdk 8 и 9. Исходный вывод получен из openjdk-9, как показано в разделе ENV. fsin, безусловно, является быстрым путем sin, но меня больше интересует, почему JIT не кэширует результат вызова метода test(), чтобы в сборке отображался только один экземпляр sin или fsin.   -  person Albert Netymk    schedule 01.01.2018
comment
Ну, это не будет кэшировать его само по себе, но оптимизирующий компилятор может удалить общее подвыражение. Я обнаружил, что в JDK8 и ранних версиях JDK9 да, но только для быстрого пути, а не для медленного пути. Я пишу более полный ответ.   -  person BeeOnRope    schedule 01.01.2018
comment
но мое ограничение состоит в том, чтобы использовать чистую Java - конечно, это, безусловно, правильный подход, я просто предупреждаю вас, чтобы вы не тратили слишком много усилий на производительность, так как в основном вы настраиваете обычный семейный автомобиль и задаетесь вопросом почему он не работает как гоночный автомобиль... ну, он никогда не будет, он построен таким с самого начала. В прошлый раз у меня были частично веские причины заботиться о производительности исходного кода Java, после завершения этой задачи я пожалел, что не переключился на нативную библиотеку C++ в самом начале, подсчитывая потраченные часы разработки после этого, чтобы выровнять, но выход будет все равно 10x.   -  person Ped7g    schedule 01.01.2018


Ответы (1)


Насколько мне известно, HotSpot не может оптимизировать избыточные вызовы чистых методов (т. е. чистые методы с идентичными аргументами), кроме как косвенно через встраивание.

То есть, если все избыточные вызовы чистого метода встроены в место вызова, избыточность обнаруживается косвенно во встроенном коде с помощью обычных оптимизаций, таких как CSE и GVN, и таким образом стоимость дополнительных вызовов обычно исчезает. Однако, если методы не встроены, я не думаю, что JVM помечает их как «чистые» и, следовательно, не может их удалить (в отличие, например, от много родных компиляторов, которые могут).

Тем не менее, учитывая, что встраивание может удалить избыточные вызовы, остается вопрос: почему избыточные вызовы Math.sin и Math.cos не встраиваются и в конечном итоге не оптимизируются?

Как оказалось, Math.sin и Math.cos, как и некоторые другие Math и другие методы в JDK, обрабатываются специально как внутренние функции. Ниже вы найдете подробный взгляд на то, что происходит в Java 8 и некоторых версиях Java 9. Показанная вами дизассемблирование взято из более поздней версии Java 9, которая обрабатывает это по-другому, что рассматривается в конце.

Способ обработки триггерных методов в JVM... сложный. В принципе, Math.sin и Math.cos встроены как встроенные методы с использованием собственных инструкций FP на x86, но есть оговорки.

В вашем тесте есть много посторонних факторов, которые затрудняют анализ, например, выделение массива, вызов Blackhole.consume, использование как Math.sin, так и Math.cos, передача константы (что может привести к тому, что некоторые триггерные инструкции будут оптимизированы). полностью), использование интерфейса A и реализация этого интерфейса и т. д.

Вместо этого давайте удалим этот хлам и сократим его до гораздо более простой версии, которая просто вызывает Math.sin(x) три раза с одинаковым аргументом и возвращает сумму:

private double i = Math.PI / 4 - 0.01;

@Benchmark
public double testMethod() {
    double res0 = Math.sin(i);
    double res1 = Math.sin(i);
    double res2 = Math.sin(i);
    return res0 + res1 + res2;
}

Выполняя это с аргументами JHM -bm avgt -tu ns -wi 5 -f 1 -i 5, я получаю около 40 нс/операций, что является нижним пределом диапазона для одного вызова fsin на современном оборудовании x86. Давайте взглянем на сборку:

[Constants]
  0x00007ff2e4dbbd20 (offset:    0): 0x54442d18   0x3fe921fb54442d18
  0x00007ff2e4dbbd24 (offset:    4): 0x3fe921fb
  0x00007ff2e4dbbd28 (offset:    8): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007ff2e4dbbd2c (offset:   12): 0xf4f4f4f4
  0x00007ff2e4dbbd30 (offset:   16): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007ff2e4dbbd34 (offset:   20): 0xf4f4f4f4
  0x00007ff2e4dbbd38 (offset:   24): 0xf4f4f4f4   0xf4f4f4f4f4f4f4f4
  0x00007ff2e4dbbd3c (offset:   28): 0xf4f4f4f4
  (snip)
[Verified Entry Point]
  0x00007ff2e4dbbd50: sub     $0x28,%rsp
  0x00007ff2e4dbbd57: mov     %rbp,0x20(%rsp)   ;*synchronization entry
                                                ; - stackoverflow.TrigBench::testMethod@-1 (line 38)

  0x00007ff2e4dbbd5c: vmovsd  0x10(%rsi),%xmm2  ;*getfield i
                                                ; - stackoverflow.TrigBench::testMethod@1 (line 38)

  0x00007ff2e4dbbd61: vmovapd %xmm2,%xmm1
  0x00007ff2e4dbbd65: sub     $0x8,%rsp
  0x00007ff2e4dbbd69: vmovsd  %xmm1,(%rsp)
  0x00007ff2e4dbbd6e: fldl    (%rsp)
  0x00007ff2e4dbbd71: fsin
  0x00007ff2e4dbbd73: fstpl   (%rsp)
  0x00007ff2e4dbbd76: vmovsd  (%rsp),%xmm1
  0x00007ff2e4dbbd7b: add     $0x8,%rsp         ;*invokestatic sin
                                                ; - stackoverflow.TrigBench::testMethod@20 (line 40)

  0x00007ff2e4dbbd7f: vmovsd  0xffffff99(%rip),%xmm3  ;   {section_word}
  0x00007ff2e4dbbd87: vandpd  0xffe68411(%rip),%xmm2,%xmm0
                                                ;   {external_word}
  0x00007ff2e4dbbd8f: vucomisd %xmm0,%xmm3
  0x00007ff2e4dbbd93: jnb     0x7ff2e4dbbe4c
  0x00007ff2e4dbbd99: vmovq   %xmm3,%r13
  0x00007ff2e4dbbd9e: vmovq   %xmm1,%rbp
  0x00007ff2e4dbbda3: vmovq   %xmm2,%rbx
  0x00007ff2e4dbbda8: vmovapd %xmm2,%xmm0
  0x00007ff2e4dbbdac: movabs  $0x7ff2f9abaeec,%r10
  0x00007ff2e4dbbdb6: callq   %r10
  0x00007ff2e4dbbdb9: vmovq   %xmm0,%r14
  0x00007ff2e4dbbdbe: vmovq   %rbx,%xmm2
  0x00007ff2e4dbbdc3: vmovq   %rbp,%xmm1
  0x00007ff2e4dbbdc8: vmovq   %r13,%xmm3
  0x00007ff2e4dbbdcd: vandpd  0xffe683cb(%rip),%xmm2,%xmm0
                                                ;*invokestatic sin
                                                ; - stackoverflow.TrigBench::testMethod@4 (line 38)
                                                ;   {external_word}
  0x00007ff2e4dbbdd5: vucomisd %xmm0,%xmm3
  0x00007ff2e4dbbdd9: jnb     0x7ff2e4dbbe56
  0x00007ff2e4dbbddb: vmovq   %xmm3,%r13
  0x00007ff2e4dbbde0: vmovq   %xmm1,%rbp
  0x00007ff2e4dbbde5: vmovq   %xmm2,%rbx
  0x00007ff2e4dbbdea: vmovapd %xmm2,%xmm0
  0x00007ff2e4dbbdee: movabs  $0x7ff2f9abaeec,%r10
  0x00007ff2e4dbbdf8: callq   %r10
  0x00007ff2e4dbbdfb: vmovsd  %xmm0,(%rsp)
  0x00007ff2e4dbbe00: vmovq   %rbx,%xmm2
  0x00007ff2e4dbbe05: vmovq   %rbp,%xmm1
  0x00007ff2e4dbbe0a: vmovq   %r13,%xmm3        ;*invokestatic sin
                                                ; - stackoverflow.TrigBench::testMethod@12 (line 39)

  0x00007ff2e4dbbe0f: vandpd  0xffe68389(%rip),%xmm2,%xmm0
                                                ;*invokestatic sin
                                                ; - stackoverflow.TrigBench::testMethod@4 (line 38)
                                                ;   {external_word}
  0x00007ff2e4dbbe17: vucomisd %xmm0,%xmm3
  0x00007ff2e4dbbe1b: jnb     0x7ff2e4dbbe32
  0x00007ff2e4dbbe1d: vmovapd %xmm2,%xmm0
  0x00007ff2e4dbbe21: movabs  $0x7ff2f9abaeec,%r10
  0x00007ff2e4dbbe2b: callq   %r10
  0x00007ff2e4dbbe2e: vmovapd %xmm0,%xmm1       ;*invokestatic sin
                                                ; - stackoverflow.TrigBench::testMethod@20 (line 40)

  0x00007ff2e4dbbe32: vmovq   %r14,%xmm0
  0x00007ff2e4dbbe37: vaddsd  (%rsp),%xmm0,%xmm0
  0x00007ff2e4dbbe3c: vaddsd  %xmm0,%xmm1,%xmm0  ;*dadd
                                                ; - stackoverflow.TrigBench::testMethod@30 (line 41)

  0x00007ff2e4dbbe40: add     $0x20,%rsp
  0x00007ff2e4dbbe44: pop     %rbp
  0x00007ff2e4dbbe45: test    %eax,0x15f461b5(%rip)  ;   {poll_return}
  0x00007ff2e4dbbe4b: retq
  0x00007ff2e4dbbe4c: vmovq   %xmm1,%r14
  0x00007ff2e4dbbe51: jmpq    0x7ff2e4dbbdcd
  0x00007ff2e4dbbe56: vmovsd  %xmm1,(%rsp)
  0x00007ff2e4dbbe5b: jmp     0x7ff2e4dbbe0f

В самом начале мы видим, что сгенерированный код загружает поле i в стек x87 FP1 и использует инструкцию fsin для вычисления Math.sin(i).


Следующая часть тоже интересна:

  0x00007ff2e4dbbd7f: vmovsd  0xffffff99(%rip),%xmm3  ;   {section_word}
  0x00007ff2e4dbbd87: vandpd  0xffe68411(%rip),%xmm2,%xmm0
                                                ;   {external_word}
  0x00007ff2e4dbbd8f: vucomisd %xmm0,%xmm3
  0x00007ff2e4dbbd93: jnb     0x7ff2e4dbbe4c

Первая инструкция загружает константу 0x3fe921fb54442d18, то есть 0.785398..., также известную как pi / 4. Второй — это vpandобъединение значения i с какой-то другой константой. Затем мы сравниваем pi / 4 с результатом vpand и прыгаем куда-нибудь, если последний меньше или равен первому.

Хм? Если вы последуете переходу, то увидите ряд (избыточных) инструкций vpandpd и vucomisd для одних и тех же значений (и использующих одну и ту же константу для vpand), что довольно быстро приводит к такой последовательности:

  0x00007ff2e4dbbe32: vmovq   %r14,%xmm0
  0x00007ff2e4dbbe37: vaddsd  (%rsp),%xmm0,%xmm0
  0x00007ff2e4dbbe3c: vaddsd  %xmm0,%xmm1,%xmm0  ;*dadd
  ...
  0x00007ff2e4dbbe4b: retq

Это просто утраивает значение, возвращаемое вызовом fsin (которое было спрятано в r14 и [rsp] во время различных переходов), и возвращается.

Таким образом, мы видим здесь, что два избыточных вызова Math.sin(i) были устранены в случае, когда "переходы выполнены", хотя устранение по-прежнему явно суммирует все значения, как если бы они были уникальными, и выполняет кучу избыточных and и инструкций сравнения. .

Если мы не совершим прыжок, мы получим то же callq %r10 поведение, которое вы демонстрируете в своем дизассемблере.

Что тут происходит?


Мы найдем просветление, если покопаемся в inline_trig вызове library_call.cpp в исходный код JVM для точки доступа. В начале этого метода мы видим это (некоторый код для краткости опущен):

  // Rounding required?  Check for argument reduction!
  if (Matcher::strict_fp_requires_explicit_rounding) {
    // (snip)

    // Pseudocode for sin:
    // if (x <= Math.PI / 4.0) {
    //   if (x >= -Math.PI / 4.0) return  fsin(x);
    //   if (x >= -Math.PI / 2.0) return -fcos(x + Math.PI / 2.0);
    // } else {
    //   if (x <=  Math.PI / 2.0) return  fcos(x - Math.PI / 2.0);
    // }
    // return StrictMath.sin(x);

    // (snip)

    // Actually, sticking in an 80-bit Intel value into C2 will be tough; it
    // requires a special machine instruction to load it.  Instead we'll try
    // the 'easy' case.  If we really need the extra range +/- PI/2 we'll
    // probably do the math inside the SIN encoding.

    // Make the merge point
    RegionNode* r = new RegionNode(3);
    Node* phi = new PhiNode(r, Type::DOUBLE);

    // Flatten arg so we need only 1 test
    Node *abs = _gvn.transform(new AbsDNode(arg));
    // Node for PI/4 constant
    Node *pi4 = makecon(TypeD::make(pi_4));
    // Check PI/4 : abs(arg)
    Node *cmp = _gvn.transform(new CmpDNode(pi4,abs));
    // Check: If PI/4 < abs(arg) then go slow
    Node *bol = _gvn.transform(new BoolNode( cmp, BoolTest::lt ));
    // Branch either way
    IfNode *iff = create_and_xform_if(control(),bol, PROB_STATIC_FREQUENT, COUNT_UNKNOWN);
    set_control(opt_iff(r,iff));

    // Set fast path result
    phi->init_req(2, n);

    // Slow path - non-blocking leaf call
    Node* call = NULL;
    switch (id) {
    case vmIntrinsics::_dsin:
      call = make_runtime_call(RC_LEAF, OptoRuntime::Math_D_D_Type(),
                               CAST_FROM_FN_PTR(address, SharedRuntime::dsin),
                               "Sin", NULL, arg, top());
      break;

      break;
    }

По сути, существует быстрый путь и медленный путь для триггерных методов — если аргумент sin больше, чем Math.PI / 4, мы используем медленный путь. Проверка включает вызов Math.abs, что и делал загадочный vandpd 0xffe68411(%rip),%xmm2,%xmm0: он очищал верхний бит, что является быстрым способом выполнения abs для значений с плавающей запятой в регистрах SSE или AVX.

Теперь остальной код тоже имеет смысл: большая часть кода, который мы видим, представляет собой три быстрых пути после оптимизации: два избыточных вызова fsin были удалены, но окружающие проверки остались. Вероятно, это просто ограничение оптимизатора: либо оптимизатор просто недостаточно силен, чтобы исключить все, либо расширение этих встроенных методов происходит после фазы оптимизации, которая объединила бы их2.

На медленном пути мы делаем вызов make_runtime_call, который отображается как callq %r10. Это так называемый вызов метода-заглушки, который внутренне реализует sin, включая проблему «уменьшения аргументов», упомянутую в комментариях. В моей системе медленный путь не обязательно намного медленнее, чем быстрый путь: если вы измените - на + при инициализации i:

private double i = Math.PI / 4 - 0.01;

вы вызываете медленный путь, который для одиночного вызова Math.sin(i) занимает ~50 нс по сравнению с 40 нс для быстрого пути3. Проблема возникает при оптимизации трех избыточных вызовов Math.sin(i). Как мы видим из приведенного выше источника, callq %r10 встречается три раза (и, прослеживая путь выполнения, мы видим, что все они выполняются после первого прыжка). Это означает, что время выполнения составляет около 150 нс для трех вызовов, или почти в 4 раза больше, чем в случае быстрого пути.

Очевидно, JDK не может в этом случае объединить узлы runtime_call, хотя они и для идентичных аргументов. Скорее всего, узлы runtime_call во внутреннем представлении относительно непрозрачны и не подлежат CSE и другим оптимизациям, которые могли бы помочь. Эти вызовы в основном используются для внутреннего расширения и некоторых внутренних методов JVM, и на самом деле они не будут ключевыми целями для этого типа оптимизации, поэтому такой подход кажется разумным.

Последняя Java 9

Все это изменилось в Java 9 noreferrer">с этим изменением.

«Быстрый путь», где fsin был непосредственно встроен, был удален. Мое использование кавычек вокруг «быстрого пути» здесь преднамеренно: есть, безусловно, основания полагать, что методы sin программного обеспечения с поддержкой SSE или AVX могут быть быстрее, чем fsin x87, который не получил большой любви более десяти лет. Действительно, это изменение заменяет вызовы fsin с использованием реализации Intel LIBM (вот алгоритм во всей красе для интересующихся).

Отлично, так что, может быть, теперь это быстрее (возможно - OP не предоставил числа даже после запроса, поэтому мы не знаем) - но побочный эффект заключается в том, что без встраивания мы всегда явно делаем вызов для каждого Math.sin и Math.cos, которые появляются в источнике: CSE не происходит.

Вероятно, вы могли бы зарегистрировать это как ошибку горячей точки, тем более что ее можно позиционировать как регрессию, хотя я подозреваю, что случаи использования, когда известные идентичные аргументы многократно передаются в триггерные функции, очень невелики. Даже законные ошибки производительности, четко объясненные и задокументированные, часто томятся годами (если, конечно, у вас нет платного контракта на поддержку с Oracle — тогда томление несколько меньше).


1 На самом деле довольно глупым, окольным способом: он начинается в памяти по адресу [rsi + 0x10], затем загружается оттуда в xmm2, затем reg-reg перемещается в xmm1 и сохраняет его назад< /em> в память наверху стека (vmovsd %xmm1,(%rsp)), а затем, наконец, загружает его в стек x87 FP с fldl (%rsp). Конечно, он мог просто загрузить его прямо из исходного местоположения в [rsp + 0x10] с помощью одного fld! Это, вероятно, добавляет 5 циклов или более к общей задержке.

2 Следует отметить, однако, что инструкция fsin доминирует здесь во время выполнения, поэтому дополнительный материал ничего не добавляет во время выполнения: если вы уменьшите метод до одной строки return Math.sin(i);, время выполнения составит около то же самое на 40 нс.

3 По крайней мере, для аргументов, близких к Math.PI / 4. За пределами этого диапазона синхронизация различна - она ​​очень быстрая для значений, близких к pi / 2 (около 40 нс - так же быстро, как «быстрый путь»), и обычно около 65 нс для очень больших значений, которые, вероятно, уменьшаются с помощью деления/модификации.

person BeeOnRope    schedule 01.01.2018