Google Analytics

суббота, 9 мая 2015 г.

Что в действительности происходит за рамками абстракций java во время исполнения

Процесс преобразования кода на языке java в инструкции, которые выполняет процессор, нетривиален. Обычно, разработчика на java мало интересует, что же происходит там, за рамками абстракции синтаксиса языка. Но как только требуется интерпретировать какой-либо феномен производительности, обосновано выбрать ту или иную возможность реализации, исследовать непонятное поведение программы или просто выяснить, что же скрывается за синтаксисом языка, необходимы какие-то пути заглянуть вглубь. О ток как это сделать и пойдет речь в этом посте.
Хорошо бы определиться какой код нам взять в качестве примера для изучения. Пусть это будет очень "простой" тест, с помощью которого можно измерить накладные расходы использования Integer по сравнению с int. (слово "простой" не случайно в кавычках). Для тех, кто не знаком с инструментом для нано/микро/мили/макро бенчмаркинга - JMH, рекомендуется взглянуть на его описание перед продолжением чтения. Итак, вот наш тест.
public class TypesComparison {

  private static final int RANDOM_INT = new Random().nextInt();
  private static final Integer RANDOM_INTEGER = RANDOM_INT;

  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public void primitiveInt() {
    int result = calculateWithInt(RANDOM_INT);
  }

  public int calculateWithInt(int randomNumber) {
    int counter = 0;
    for (int i = 0; i < 10000; i++) {
      counter += randomNumber;
    }
    return counter;
  }
  
  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public void wrapperInteger() {
    int result = calculateWithInteger(RANDOM_INTEGER);
  }

  public int calculateWithInteger(Integer randomNumber) {
    Integer counter = 0;
    for (int i = 0; i < 10000; i++) {
      counter += randomNumber;
    }
    return counter;
  }

}
Рассмотрим два метода calculateWithInt и calculateWithInteger. Единственное их различие это используемые типы входного и выходного параметров и внутренней переменной. Как было упомянуто выше, тест сравнивает накладные расходы оберточного типа Integer с простым типом int. Думаю, очевидно, что там должна быть разница. Но вот какая она. Давайте запустим тест и посмотрим результаты.
java -jar java/target/benchmarks.jar -f=1
Результаты выглядят так:
Benchmark             Mode  Cnt      Score     Error  Units
Types.primitiveInt    avgt   20      0.308 ±   0.001  ns/op
Types.wrapperInteger  avgt   20  27714.447 ± 252.660  ns/op
Что?! Почему разница в пять порядков? Можем ли мы сделать вывод, что арифметические операции над Integer на пять порядков медленнее чем int? Давайте разберемся.

Байткод JVM

Первое и самое простое, что мы можем сделать, это посмотреть байткод, в который скомпилирован вышеуказанный класс TypesComparison. Вероятно, большинство из читателей, в той или иной мере, уже знакомо с javap. Эта утилита выводит в читаемом виде скомпилированый байткод.
javap -l -c -s target/classes/examples/java/TypesComparison.class

public int calculateWithInt(int);
    descriptor: (I)I
    Code:
       0: iconst_0
       1: istore_2
       2: iconst_0
       3: istore_3
       4: goto          14
       7: iload_2
       8: iload_1
       9: iadd
      10: istore_2
      11: iinc          3, 1
      14: iload_3
      15: sipush        10000
      18: if_icmplt     7
      21: iload_2
      22: ireturn
    LineNumberTable:
      line 24: 0
      line 25: 2
      line 26: 7
      line 25: 11
      line 28: 21
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      23     0  this   Lexamples/java/TypesComparison;
          0      23     1 randomNumber   I
          2      21     2 counter   I
          4      17     3     i   I

public int calculateWithInteger(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)I
    Code:
       0: iconst_0
       1: invokestatic  #23                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_2
       5: iconst_0
       6: istore_3
       7: goto          26
      10: aload_2
      11: invokevirtual #60                 // Method java/lang/Integer.intValue:()I
      14: aload_1
      15: invokevirtual #60                 // Method java/lang/Integer.intValue:()I
      18: iadd
      19: invokestatic  #23                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      22: astore_2
      23: iinc          3, 1
      26: iload_3
      27: sipush        10000
      30: if_icmplt     10
      33: aload_2
      34: invokevirtual #60                 // Method java/lang/Integer.intValue:()I
      37: ireturn
    LineNumberTable:
      line 40: 0
      line 41: 5
      line 42: 10
      line 41: 23
      line 44: 33
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      38     0  this   Lexamples/java/TypesComparison;
          0      38     1 randomNumber   Ljava/lang/Integer;
          5      33     2 counter   Ljava/lang/Integer;
          7      26     3     i   I
Ну что же, давайте интерпретировать результаты. Секция LineNumberTable показывает связь между командами jvm и строчками java кода. Для полного изучения набора инструкций jvm, смотрите в The Java Virtual Machine Specification Java SE 8 Edition Отличия между двумя функциями кроются в нескольких инструкциях. Это istore в случае int и инструкции astore в случае Integer. К тому же второй случай имеет дополнительные вызовы статических и виртуальных методов. Давайте взглянем на описание инструкции в спецификации.

  • istore_<n> - Store int into local variable. The <n> must be an index into the local variable array of the current frame.
  • iload_<n> - Load int from local variable. The <n> must be an index into the local variable array of the current frame.
  • astore_<n> - Store reference into local variable. The <n> must be an index into the local variable array of the current frame.
  • aload_<n> - Load reference from local variable. The <n> must be an index into the local variable array of the current frame.
  • invokestatic - Invoke a class (static) method.
  • invokevirtual - Invoke instance method; dispatch based on class.
  • if_icmp<cond> - Branch if int comparison succeeds. Both value1 and value2 must be of type int. They are both popped from the operand stack and compared. All comparisons are signed. The results of the comparison are as follows: if_icmplt succeeds if and only if value1 < value2

Я намеренно опустил технические детали из описания методов, которые не столь важны в этом контексте.
На этом этапе должно быть хорошо видно, что второй вариант использует методы класса Integer для того чтобы извлекать и сохранять значения. А все вычисления по прежнему осуществляются над int значениями. Всё выглядит так, как мы и предполагали. На этом можно было бы остановиться. Но возникает вопрос, а почему же извлечение и сохранение ссылочных типов, а также вызовы статических и виртуальных методов так дороги. К сожалению, с помощью javap мы не можем выяснить, что же происходит при выполнений инструкций jvm. Для этого нам нужно спуститься на еще один уровень ниже.

Дизассемблирование сгенерированного JIT компилятором кода

Для того чтобы увидеть дизассемблированный код который выполняет процессор, воспользуемся плагином jvm - hsdis. Его нужно собрать и поместить в директорию с разделяемыми библиотеками в JAVA_HOME. В этом вам поможет его README.
Выполним дизассемблирвоание двух методов (calculateWithInt и calculateWithInteger) из нашего теста. Каждый раз фильтруем вывод только для соответствующего метода, иначе он будет просто огромным. Тест запускается в молчаливом режиме (-v SILENT), чтобы не смешивать его вывод и вывод дизассемблера.
java -jar java/target/benchmarks.jar -jvm java -jvmArgsPrepend "-XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=intel -XX:CompileCommand=print,*TypesComparison.calculateWithInt" -v SILENT .*primitiveInt

[Verified Entry Point]
  ;; B1: #      B2 <- BLOCK HEAD IS JUNK   Freq: 1

  0x00007f6be6962ac0: mov    DWORD PTR [rsp-0x16000],eax
  0x00007f6be6962ac7: push   rbp
  0x00007f6be6962ac8: sub    rsp,0x10           ;*synchronization entry
                                                ; - examples.java.TypesComparison::calculateWithInt@-1 (line 24)

  0x00007f6be6962acc: mov    r10d,edx
  0x00007f6be6962acf: add    r10d,edx
  0x00007f6be6962ad2: add    r10d,r10d
  0x00007f6be6962ad5: add    r10d,r10d
  0x00007f6be6962ad8: add    r10d,r10d
  0x00007f6be6962adb: mov    r8d,0x1
  0x00007f6be6962ae1: mov    eax,edx
  0x00007f6be6962ae3: nop
  ;; unmeaning lines are removed
  0x00007f6be6962aef: nop                       ;*iload_2
                                                ; - examples.java.TypesComparison::calculateWithInt@11 (line 26)

  ;; B2: #      B2 B3 <- B1 B2  Loop: B2-B2 inner main of N14 Freq: 10192.7

  0x00007f6be6962af0: add    eax,r10d           ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInt@13 (line 26)

  0x00007f6be6962af3: add    r8d,0x10           ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInt@15 (line 25)

  0x00007f6be6962af7: cmp    r8d,0x2701
  0x00007f6be6962afe: jl     0x00007f6be6962af0  ;*if_icmpge
                                                ; - examples.java.TypesComparison::calculateWithInt@8 (line 25)

  ;; B3: #      B6 B4 <- B2  Freq: 1

  0x00007f6be6962b00: cmp    r8d,0x2710
  0x00007f6be6962b07: jge    0x00007f6be6962b1a
  ;; B4: #      B5 <- B3  Freq: 0.5

  0x00007f6be6962b09: nop
  0x00007f6be6962b0a: nop
  0x00007f6be6962b0b: nop                       ;*iload_2
                                                ; - examples.java.TypesComparison::calculateWithInt@11 (line 26)

  ;; B5: #      B5 B6 <- B4 B5  Loop: B5-B5 inner post of N90 Freq: 1

  0x00007f6be6962b0c: add    eax,edx            ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInt@13 (line 26)

  0x00007f6be6962b0e: inc    r8d                ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInt@15 (line 25)

  0x00007f6be6962b11: cmp    r8d,0x2710
  0x00007f6be6962b18: jl     0x00007f6be6962b0c  ;*if_icmpge
                                                ; - examples.java.TypesComparison::calculateWithInt@8 (line 25)

  ;; B6: #      N67 <- B5 B3  Freq: 1

  0x00007f6be6962b1a: add    rsp,0x10
  0x00007f6be6962b1e: pop    rbp
  0x00007f6be6962b1f: test   DWORD PTR [rip+0x1224f4db],eax        # 0x00007f6bf8bb2000
                                                ;   {poll_return}
  0x00007f6be6962b25: ret    
  0x00007f6be6962b26: hlt    
  ;; некоторые строки удалены
java -jar java/target/benchmarks.jar -jvm java -jvmArgsPrepend "-XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=intel -XX:CompileCommand=print,*TypesComparison.calculateWithInteger" -v SILENT .*wrapperInteger

  [Verified Entry Point]
  ;; B1: #      B36 B2 <- BLOCK HEAD IS JUNK   Freq: 1

  0x00007fc58e493c20: mov    DWORD PTR [rsp-0x16000],eax
  0x00007fc58e493c27: push   rbp
  0x00007fc58e493c28: sub    rsp,0x30           ;*synchronization entry
                                                ; - examples.java.TypesComparison::calculateWithInteger@-1 (line 40)

  0x00007fc58e493c2c: mov    ebp,DWORD PTR [rdx+0xc]  ;*getfield value
                                                ; - java.lang.Integer::intValue@1 (line 893)
                                                ; - examples.java.TypesComparison::calculateWithInteger@19 (line 42)
                                                ; implicit exception: dispatches to 0x00007fc58e493f2a
  ;; B2: #      B30 B3 <- B1  Freq: 0.999999

  0x00007fc58e493c2f: mov    r10d,ebp
  0x00007fc58e493c32: add    r10d,0x80
  0x00007fc58e493c39: mov    r11d,0xf800221c    ;   {metadata('java/lang/Integer')}
  0x00007fc58e493c3f: movabs r8,0x0
  0x00007fc58e493c49: lea    r8,[r8+r11*8]
  0x00007fc58e493c4d: cmp    r10d,0x100
  0x00007fc58e493c54: jb     0x00007fc58e493e8b  ;*if_icmpgt
                                                ; - java.lang.Integer::valueOf@10 (line 830)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B3: #      B32 B4 <- B2  Freq: 0.999848

  0x00007fc58e493c5a: mov    rax,QWORD PTR [r15+0x70]
  0x00007fc58e493c5e: mov    r10,rax
  0x00007fc58e493c61: add    r10,0x10
  0x00007fc58e493c65: cmp    r10,QWORD PTR [r15+0x80]
  0x00007fc58e493c6c: jae    0x00007fc58e493eca
  ;; B4: #      B5 <- B3  Freq: 0.999748

  0x00007fc58e493c72: mov    QWORD PTR [r15+0x70],r10
  0x00007fc58e493c76: prefetchnta BYTE PTR [r10+0xc0]
  0x00007fc58e493c7e: mov    r10,QWORD PTR [r8+0xb0]
  0x00007fc58e493c85: mov    QWORD PTR [rax],r10
  0x00007fc58e493c88: mov    DWORD PTR [rax+0x8],0xf800221c
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {metadata('java/lang/Integer')}
  ;; B5: #      B6 <- B33 B4  Freq: 0.999848

  0x00007fc58e493c8f: mov    DWORD PTR [rax+0xc],ebp  ;*invokestatic valueOf
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B6: #      B10 <- B30 B5  Freq: 0.999999

  0x00007fc58e493c92: mov    r10d,DWORD PTR [rax+0xc]  ;*getfield value
                                                ; - java.lang.Integer::intValue@1 (line 893)
                                                ; - examples.java.TypesComparison::calculateWithInteger@34 (line 44)

  0x00007fc58e493c96: mov    r9d,0x1
  0x00007fc58e493c9c: jmp    0x00007fc58e493cd6
  0x00007fc58e493c9e: nop
  0x00007fc58e493c9f: nop
  ;; B7: #      B8 <- B15  top-of-loop Freq: 10013.5

  0x00007fc58e493ca0: mov    QWORD PTR [r15+0x70],r10
  0x00007fc58e493ca4: prefetchnta BYTE PTR [r10+0xc0]
  0x00007fc58e493cac: mov    r10,QWORD PTR [r8+0xb0]
  0x00007fc58e493cb3: mov    QWORD PTR [rax],r10
  0x00007fc58e493cb6: mov    DWORD PTR [rax+0x8],0xf800221c
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {metadata('java/lang/Integer')}
  ;; B8: #      B9 <- B17 B7  top-of-loop Freq: 10014.5

  0x00007fc58e493cbd: mov    DWORD PTR [rax+0xc],r11d  ;*invokestatic valueOf
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B9: #      B22 B10 <- B18 B8  Freq: 10016

  0x00007fc58e493cc1: mov    r10d,DWORD PTR [rax+0xc]  ;*getfield value
                                                ; - java.lang.Integer::intValue@1 (line 893)
                                                ; - examples.java.TypesComparison::calculateWithInteger@34 (line 44)

  0x00007fc58e493cc5: add    r9d,0x2            ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInteger@27 (line 41)

  0x00007fc58e493cc9: cmp    r9d,0x270f
  0x00007fc58e493cd0: jge    0x00007fc58e493e12  ;*aload_2
                                                ; - examples.java.TypesComparison::calculateWithInteger@14 (line 42)

  ;; B10: #     B19 B11 <- B6 B9        Loop: B10-B9 inner main of N50 Freq: 10016

  0x00007fc58e493cd6: mov    r11d,DWORD PTR [rdx+0xc]  ;*getfield value
                                                ; - java.lang.Integer::intValue@1 (line 893)
                                                ; - examples.java.TypesComparison::calculateWithInteger@19 (line 42)

  0x00007fc58e493cda: add    r10d,r11d          ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInteger@22 (line 42)

  0x00007fc58e493cdd: mov    ecx,r10d
  0x00007fc58e493ce0: add    ecx,0x80
  0x00007fc58e493ce6: cmp    ecx,0x100
  0x00007fc58e493cec: jb     0x00007fc58e493dbe  ;*if_icmpgt
                                                ; - java.lang.Integer::valueOf@10 (line 830)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B11: #     B20 B12 <- B10  Freq: 10014.5

  0x00007fc58e493cf2: mov    rax,QWORD PTR [r15+0x70]
  0x00007fc58e493cf6: mov    r11,rax
  0x00007fc58e493cf9: add    r11,0x10
  0x00007fc58e493cfd: cmp    r11,QWORD PTR [r15+0x80]
  0x00007fc58e493d04: jae    0x00007fc58e493ddc
  ;; B12: #     B13 <- B11  Freq: 10013.5

  0x00007fc58e493d0a: mov    QWORD PTR [r15+0x70],r11
  0x00007fc58e493d0e: prefetchnta BYTE PTR [r11+0xc0]
  0x00007fc58e493d16: mov    r11,QWORD PTR [r8+0xb0]
  0x00007fc58e493d1d: mov    QWORD PTR [rax],r11
  0x00007fc58e493d20: mov    DWORD PTR [rax+0x8],0xf800221c
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {metadata('java/lang/Integer')}
  ;; B13: #     B14 <- B21 B12  Freq: 10014.5

  0x00007fc58e493d27: mov    DWORD PTR [rax+0xc],r10d  ;*invokespecial <init>
                                                ; - java.lang.Integer::valueOf@28 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  0x00007fc58e493d2b: mov    r11d,DWORD PTR [rdx+0xc]  ;*invokestatic valueOf
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B14: #     B18 B15 <- B19 B13  Freq: 10016

  0x00007fc58e493d2f: add    r11d,DWORD PTR [rax+0xc]  ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInteger@22 (line 42)

  0x00007fc58e493d33: mov    r10d,r11d
  0x00007fc58e493d36: add    r10d,0x80
  0x00007fc58e493d3d: cmp    r10d,0x100
  0x00007fc58e493d44: jb     0x00007fc58e493da0  ;*if_icmpgt
                                                ; - java.lang.Integer::valueOf@10 (line 830)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B15: #     B7 B16 <- B14  Freq: 10014.5

  0x00007fc58e493d46: mov    rax,QWORD PTR [r15+0x70]
  0x00007fc58e493d4a: mov    r10,rax
  0x00007fc58e493d4d: add    r10,0x10
  0x00007fc58e493d51: cmp    r10,QWORD PTR [r15+0x80]
  0x00007fc58e493d58: jb     0x00007fc58e493ca0
  ;; B16: #     B40 B17 <- B15  Freq: 1.00162

  0x00007fc58e493d5e: mov    DWORD PTR [rsp+0x14],r11d
  0x00007fc58e493d63: mov    QWORD PTR [rsp+0x8],r8
  0x00007fc58e493d68: mov    QWORD PTR [rsp],rdx
  0x00007fc58e493d6c: mov    ebp,r9d
  0x00007fc58e493d6f: mov    DWORD PTR [rsp+0x10],r9d
  0x00007fc58e493d74: inc    ebp                ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInteger@27 (line 41)

  0x00007fc58e493d76: movabs rsi,0x7c00110e0    ;   {metadata('java/lang/Integer')}
  0x00007fc58e493d80: nop
  0x00007fc58e493d81: nop
  0x00007fc58e493d82: nop
  0x00007fc58e493d83: call   0x00007fc58e368c60  ; OopMap{[0]=Oop off=392}
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {runtime_call}
  ;; B17: #     B8 <- B16  Freq: 1.0016

  0x00007fc58e493d88: mov    rdx,QWORD PTR [rsp]
  0x00007fc58e493d8c: mov    r8,QWORD PTR [rsp+0x8]
  0x00007fc58e493d91: mov    r9d,DWORD PTR [rsp+0x10]
  0x00007fc58e493d96: mov    r11d,DWORD PTR [rsp+0x14]
  0x00007fc58e493d9b: jmp    0x00007fc58e493cbd
  ;; B18: #     B9 <- B14  Freq: 1.51639

  0x00007fc58e493da0: movsxd r10,r11d
  0x00007fc58e493da3: movabs r11,0x76cd2da08    ;   {oop(a 'java/lang/Integer'[256] )}
  0x00007fc58e493dad: mov    r10d,DWORD PTR [r11+r10*4+0x210]
  0x00007fc58e493db5: lea    rax,[r12+r10*8]
  0x00007fc58e493db9: jmp    0x00007fc58e493cc1
  ;; B19: #     B14 <- B10  Freq: 1.51639

  0x00007fc58e493dbe: movsxd r10,r10d
  0x00007fc58e493dc1: movabs rcx,0x76cd2da08    ;   {oop(a 'java/lang/Integer'[256] )}
  0x00007fc58e493dcb: mov    ecx,DWORD PTR [rcx+r10*4+0x210]
  0x00007fc58e493dd3: lea    rax,[r12+rcx*8]    ;*aaload
                                                ; - java.lang.Integer::valueOf@21 (line 831)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  0x00007fc58e493dd7: jmp    0x00007fc58e493d2f
  ;; B20: #     B39 B21 <- B11  Freq: 1.00162

  0x00007fc58e493ddc: mov    DWORD PTR [rsp+0xc],r9d
  0x00007fc58e493de1: mov    DWORD PTR [rsp+0x8],r10d
  0x00007fc58e493de6: mov    QWORD PTR [rsp],r8
  0x00007fc58e493dea: mov    rbp,rdx
  0x00007fc58e493ded: movabs rsi,0x7c00110e0    ;   {metadata('java/lang/Integer')}
  0x00007fc58e493df7: call   0x00007fc58e368c60  ; OopMap{rbp=Oop off=508}
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {runtime_call}
  ;; B21: #     B13 <- B20  Freq: 1.0016

  0x00007fc58e493dfc: mov    rdx,rbp
  0x00007fc58e493dff: mov    r8,QWORD PTR [rsp]
  0x00007fc58e493e03: mov    r10d,DWORD PTR [rsp+0x8]
  0x00007fc58e493e08: mov    r9d,DWORD PTR [rsp+0xc]
  0x00007fc58e493e0d: jmp    0x00007fc58e493d27
  ;; B22: #     B29 B23 <- B9  Freq: 0.999979

  0x00007fc58e493e12: cmp    r9d,0x2710
  0x00007fc58e493e19: jge    0x00007fc58e493e7c
  ;; B23: #     B24 <- B22  Freq: 0.499989

  0x00007fc58e493e1b: nop                       ;*aload_2
                                                ; - examples.java.TypesComparison::calculateWithInteger@14 (line 42)

  ;; B24: #     B31 B25 <- B23 B28      Loop: B24-B28 inner post of N339 Freq: 0.999979

  0x00007fc58e493e1c: add    r10d,DWORD PTR [rdx+0xc]  ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInteger@22 (line 42)

  0x00007fc58e493e20: mov    r11d,r10d
  0x00007fc58e493e23: add    r11d,0x80
  0x00007fc58e493e2a: cmp    r11d,0x100
  0x00007fc58e493e31: jb     0x00007fc58e493eac  ;*if_icmpgt
                                                ; - java.lang.Integer::valueOf@10 (line 830)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B25: #     B34 B26 <- B24  Freq: 0.999828

  0x00007fc58e493e33: mov    rax,QWORD PTR [r15+0x70]
  0x00007fc58e493e37: mov    r11,rax
  0x00007fc58e493e3a: add    r11,0x10
  0x00007fc58e493e3e: cmp    r11,QWORD PTR [r15+0x80]
  0x00007fc58e493e45: jae    0x00007fc58e493ef2
  ;; B26: #     B27 <- B25  Freq: 0.999728

  0x00007fc58e493e4b: mov    QWORD PTR [r15+0x70],r11
  0x00007fc58e493e4f: prefetchnta BYTE PTR [r11+0xc0]
  0x00007fc58e493e57: mov    r11,QWORD PTR [r8+0xb0]
  0x00007fc58e493e5e: mov    QWORD PTR [rax],r11
  0x00007fc58e493e61: mov    DWORD PTR [rax+0x8],0xf800221c
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {metadata('java/lang/Integer')}
  ;; B27: #     B28 <- B35 B26  Freq: 0.999828

  0x00007fc58e493e68: mov    DWORD PTR [rax+0xc],r10d  ;*invokestatic valueOf
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  ;; B28: #     B24 B29 <- B31 B27  Freq: 0.999979

  0x00007fc58e493e6c: mov    r10d,DWORD PTR [rax+0xc]  ;*getfield value
                                                ; - java.lang.Integer::intValue@1 (line 893)
                                                ; - examples.java.TypesComparison::calculateWithInteger@34 (line 44)

  0x00007fc58e493e70: inc    r9d                ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInteger@27 (line 41)

  0x00007fc58e493e73: cmp    r9d,0x2710
  0x00007fc58e493e7a: jl     0x00007fc58e493e1c  ;*if_icmpge
                                                ; - examples.java.TypesComparison::calculateWithInteger@11 (line 41)

  ;; B29: #     N463 <- B28 B22  Freq: 0.999979

  0x00007fc58e493e7c: mov    eax,r10d
  0x00007fc58e493e7f: add    rsp,0x30
  0x00007fc58e493e83: pop    rbp
  0x00007fc58e493e84: test   DWORD PTR [rip+0x1224a176],eax        # 0x00007fc5a06de000
                                                ;   {poll_return}
  0x00007fc58e493e8a: ret    
  ;; B30: #     B6 <- B2  Freq: 0.000151396

  0x00007fc58e493e8b: movsxd r10,ebp
  0x00007fc58e493e8e: movabs r11,0x76cd2da08    ;   {oop(a 'java/lang/Integer'[256] )}
  0x00007fc58e493e98: mov    r10d,DWORD PTR [r11+r10*4+0x210]
  0x00007fc58e493ea0: shl    r10,0x3
  0x00007fc58e493ea4: mov    rax,r10
  0x00007fc58e493ea7: jmp    0x00007fc58e493c92
  ;; B31: #     B28 <- B24  Freq: 0.000151393

  0x00007fc58e493eac: movsxd r10,r10d
  0x00007fc58e493eaf: movabs r11,0x76cd2da08    ;   {oop(a 'java/lang/Integer'[256] )}
  0x00007fc58e493eb9: mov    r11d,DWORD PTR [r11+r10*4+0x210]
  0x00007fc58e493ec1: lea    r10,[r12+r11*8]    ;*aaload
                                                ; - java.lang.Integer::valueOf@21 (line 831)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)

  0x00007fc58e493ec5: mov    rax,r10
  0x00007fc58e493ec8: jmp    0x00007fc58e493e6c
  ;; B32: #     B38 B33 <- B3  Freq: 0.000100001

  0x00007fc58e493eca: mov    QWORD PTR [rsp+0x8],r8
  0x00007fc58e493ecf: mov    QWORD PTR [rsp],rdx
  0x00007fc58e493ed3: movabs rsi,0x7c00110e0    ;   {metadata('java/lang/Integer')}
  0x00007fc58e493edd: nop
  0x00007fc58e493ede: nop
  0x00007fc58e493edf: call   0x00007fc58e368c60  ; OopMap{[0]=Oop off=740}
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {runtime_call}
  ;; B33: #     B5 <- B32  Freq: 9.99993e-05

  0x00007fc58e493ee4: mov    rdx,QWORD PTR [rsp]
  0x00007fc58e493ee8: mov    r8,QWORD PTR [rsp+0x8]
  0x00007fc58e493eed: jmp    0x00007fc58e493c8f
  ;; B34: #     B37 B35 <- B25  Freq: 9.99993e-05

  0x00007fc58e493ef2: mov    DWORD PTR [rsp+0xc],r9d
  0x00007fc58e493ef7: mov    DWORD PTR [rsp+0x8],r10d
  0x00007fc58e493efc: mov    QWORD PTR [rsp],r8
  0x00007fc58e493f00: mov    rbp,rdx
  0x00007fc58e493f03: movabs rsi,0x7c00110e0    ;   {metadata('java/lang/Integer')}
  0x00007fc58e493f0d: nop
  0x00007fc58e493f0e: nop
  0x00007fc58e493f0f: call   0x00007fc58e368c60  ; OopMap{rbp=Oop off=788}
                                                ;*new  ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {runtime_call}
  ;; B35: #     B27 <- B34  Freq: 9.99973e-05

  0x00007fc58e493f14: mov    rdx,rbp
  0x00007fc58e493f17: mov    r8,QWORD PTR [rsp]
  0x00007fc58e493f1b: mov    r10d,DWORD PTR [rsp+0x8]
  0x00007fc58e493f20: mov    r9d,DWORD PTR [rsp+0xc]
  0x00007fc58e493f25: jmp    0x00007fc58e493e68
  ;; B36: #     N463 <- B1  Freq: 1.01328e-06

  0x00007fc58e493f2a: movabs r10,0x76cd2da08    ;   {oop(a 'java/lang/Integer'[256] )}
  0x00007fc58e493f34: mov    ebp,DWORD PTR [r10+0x210]
                                                ;*aaload
                                                ; - java.lang.Integer::valueOf@21 (line 831)
                                                ; - examples.java.TypesComparison::calculateWithInteger@1 (line 40)

  0x00007fc58e493f3b: mov    esi,0xffffff86
  0x00007fc58e493f40: mov    QWORD PTR [rsp],rdx
  0x00007fc58e493f44: nop
  0x00007fc58e493f45: nop
  0x00007fc58e493f46: nop
  0x00007fc58e493f47: call   0x00007fc58e30c820  ; OopMap{rbp=NarrowOop [0]=Oop off=844}
                                                ;*aload_2
                                                ; - examples.java.TypesComparison::calculateWithInteger@14 (line 42)
                                                ;   {runtime_call}
  0x00007fc58e493f4c: call   0x00007fc59f00c71a  ;   {runtime_call}
  ;; B37: #     B42 <- B34  Freq: 9.99993e-10

  0x00007fc58e493f51: mov    rsi,rax
  0x00007fc58e493f54: jmp    0x00007fc58e493f5d
  ;; B38: #     B41 <- B32  Freq: 1.00001e-09

  0x00007fc58e493f56: jmp    0x00007fc58e493f5a
  ;; B39: #     B41 <- B20  Freq: 1.00162e-05

  0x00007fc58e493f58: jmp    0x00007fc58e493f5a
  ;; B40: #     B41 <- B16  Freq: 1.00162e-05

  ;; B41: #     B42 <- B38 B39 B40  Freq: 2.00334e-05

  0x00007fc58e493f5a: mov    rsi,rax
  ;; B42: #     N463 <- B37 B41  Freq: 2.00344e-05

  0x00007fc58e493f5d: add    rsp,0x30
  0x00007fc58e493f61: pop    rbp
  0x00007fc58e493f62: jmp    0x00007fc58e405d20  ;*new
                                                ; - java.lang.Integer::valueOf@23 (line 832)
                                                ; - examples.java.TypesComparison::calculateWithInteger@23 (line 42)
                                                ;   {runtime_call}
  0x00007fc58e493f67: hlt    
  ;; некоторые строки удалены
По аналогии с разбором байткода jvm, нам необходим справочник команд. В данном случае это справочник команд процессора. Intel предоставляет его Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual Обратите внимание, что в командной строке указано использовать синтаксис intel -XX:PrintAssemblyOptions=intel, по умолчанию это синтаксис AT&T. Вы можете выбрать тот, который вам больше знаком. Для удобства, в выводе добавлены комментарии со ссылками на инструкции байткода. Стоит отметить, что обычно, разобраться с логикой ассемблерного кода сложнее чем с байткодом jvm. Этому способствует несколько причин. Рассмотрим парочку. Первая - ассемблерный код имеет гораздо большее количество операций который к тому же зависят от процессора исполняющего наш код (например поддержка SSE4 И AVX инструкций). В силу простоты теста, в нашем случае отсутствует необходимость в таких инструкциях (целочисленная арифметика). Вторая причина, оптимизации JIT компилятора основанные на информации собранной во время выполнения программы, которые к тому же могут изменяться в зависимости от изменения поведения программы. С целью сократить размер описания сфокусируемся лишь на наиболее важных местах.
Давай определим место с которого мы начнем изучение листингов ассемблерного кода? Первым делом давайте посмотрим на блоки которые вызываются максимальное число раз. Отличить их просто, в их заголовке присутствует комментарий со строкой вида ";; B13: #     B14 <- B21 B12  Freq: 10014.5" Нетрудно догадаться, что необходимо смотреть на значение частоты выполнения (Freq). Оно очень близко к значению констант указанному в for циклах сравниваемых методов "for (int i = 0; i < 10000; i++)". По таким блокам можно выделить тело цикла. Внимательный читатель вероятно успел заметить, что тело цикла разбито на множество блоков с переходами между ними. В некоторых случаях, условия перехода выглядят странно, как например строка "0x00007f6be6962af7: cmp    r8d,0x2701" из первого листинга. В регистре r8d находится значение счетчика for цикла (переменная i в java коде) и оно сравнивается с 9985 в десятичной системе счисления. Что же это такое? Это оптимизация, которую выполнил JIT компилятор. Итоговый код выполняет итерирование не с шагом 1, а с шагом 16. Перед телом цикла находится код инициализации значений переменных (0x00007f6be6962acc). Регистр r10d содержит значение приращения переменной counter с учетом нового шага 16 (16 * randomNumber). А регистр r8d будет содержать значение переменной i.
  0x00007f6be6962acc: mov    r10d,edx
  0x00007f6be6962acf: add    r10d,edx
  0x00007f6be6962ad2: add    r10d,r10d
  0x00007f6be6962ad5: add    r10d,r10d
  0x00007f6be6962ad8: add    r10d,r10d
  0x00007f6be6962adb: mov    r8d,0x1
Далее следует итерирование по новому шагу до тех пор пока r8d строго меньше 9985 (10000-16=9984)
  ;; B2: #      B2 B3 <- B1 B2  Loop: B2-B2 inner main of N14 Freq: 10192.7

  0x00007f6be6962af0: add    eax,r10d           ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInt@13 (line 26)

  0x00007f6be6962af3: add    r8d,0x10           ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInt@15 (line 25)

  0x00007f6be6962af7: cmp    r8d,0x2701
  0x00007f6be6962afe: jl     0x00007f6be6962af0  ;*if_icmpge
                                                ; - examples.java.TypesComparison::calculateWithInt@8 (line 25)
И в завершении - возвращение результата из функции, которое не содержит ничего интересного. К слову сказать, ниже по коду присутствует фрагмент содержащий команды тела цикла без оптимизации.
  ;; B5: #      B5 B6 <- B4 B5  Loop: B5-B5 inner post of N90 Freq: 1

  0x00007f6be6962b0c: add    eax,edx            ;*iadd
                                                ; - examples.java.TypesComparison::calculateWithInt@13 (line 26)

  0x00007f6be6962b0e: inc    r8d                ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInt@15 (line 25)

  0x00007f6be6962b11: cmp    r8d,0x2710
  0x00007f6be6962b18: jl     0x00007f6be6962b0c  ;*if_icmpge
                                                ; - examples.java.TypesComparison::calculateWithInt@8 (line 25)
Подведем итог дизассемблирования метода оперирующего int переменными. Вычисления осуществляются в одном регистре eax, в котором накапливается сумма. Целочисленная арифметика на регистрах общего назначения - что может быть проще. В дополнение к этому присутствует оптимизация, уменьшающая количество итераций.
Начнем рассмотрение метода использующего Integer. Блок B7 соответствует телу цикла. Первая инструкция (0x00007fc58e493ca0) помешает значение из регистра r10 в адресс QWORD PTR [r15+0x70] (четверное слово - 64 бита). Это адрес памяти, где храниться объект Integer из переменной counter. Далее следует инструкция указывающая механизму кэширования процессора сохранять эти данные как можно ближе (см. уровни кэширования).

  • prefetchnta - Non-temporal data—fetch data into location close to the processor, minimizing cache pollution

Стоит подчеркнуть, что код работы с Integer выглядит достаточно сложным в этом месте, по причине наличия оптимизации связанной с кэшированием значений Integer (см. private static class IntegerCache из реализации класса Inetger). Суть оптимизации заключается в хранении некоторого количества заранее созданных объектов Integer. По умолчанию это интервал значений [-128, 127]. Этот интервал можно переопределить через свойство java.lang.Integer.IntegerCache.high при запуске jvm процесса.
Арифметика над Integer выглядит следующим образом, распаковка значения java.lang.Integer::intValue обычно заключается в обращении к конкретным ячейкам памяти (не забываем, что это кэшируется процессором) как в следующем примере:
0x00007fc58e493cc1: mov    r10d,DWORD PTR [rax+0xc]
Арифметические действия над int значениями выполняются командами процессора, которые оперируют значениями в регистрах общего назначения.
0x00007fc58e493cda: add    r10d,r11d
В данной строке происходит сложение counter += randomNumber. Далее следует либо обращение к кешированым значением Integer, либо создание нового значения Integer из int хранящегося в регистре. А это инициализация памяти объекта и сохранение в него значения (все это операции с памятью).Мы пропустили строки:
0x00007fc58e493cc5: add    r9d,0x2            ;*iinc
                                                ; - examples.java.TypesComparison::calculateWithInteger@27 (line 41)

0x00007fc58e493cc9: cmp    r9d,0x270f
0x00007fc58e493cd0: jge    0x00007fc58e493e12  ;*aload_2
Это инкрементирование счетчика for цикла (переменная i). В этом месте можно увидеть уже знакомую нам оптимизацию (описана выше для метода calculateWithInt). Отличие заключается в инкрементирование с шагом 0x2. Другими словами, цикл оптимизирован сокращением количество итераций в два раза.
И вот настал момент, когда мы можем сделать выводы о причинах такой разницы производительности тестовых методов:

  • Оптимизация циклов привела к существенно отличному числу итераций (625 против 5000)
  • Арифметические операции над Integer существенно дороже, так как привносят избыточные расходы на обращение к памяти (создание объектов, чтение значений int) по сравнению с операциями на одном регистре.

Стоит явно подчеркнуть, что это результат сравнения только конкретного приведенного кода, а не сравнение производительности использования типов int и Integer в общем виде. Если избыточные расходы на арифметические операции над Integer присутствуют в любом использующем их коде, то в случае с циклами поведение зависит от конкретной логики. И конечно же это очень показательный пример демонстрирующий, что понимание всех слоев абстракции и фактически выполняемого кода позволяет корректно интерпертировать наблюдаемые феномены. Невозможно объяснить подобное поведение основываясь только на уровне языка.

Отладка Hotspot

В предыдущей секции мы выяснили причины различия производительности двух тестовых методов. На этом можно было бы остановиться. Но остается вопрос - какова природа оптимизации циклов и почему мы видим сокращение количества итераций в 2 и в 16 раз. Главный источник истины, который даст ответ на тот вопрос - код jvm. Возможно ограничиться рассмотрением исходного кода, но выполнение кода jvm в голове не слишком простая задача. Поэтому давайте попробуем запустить jvm в отладчике и пошагово выполнить код оптимизирующий циклы.
Для того, чтобы иметь возможность отлаживать jvm необходимо собрать свою версию с включенной отладочной информацией. В первую очередь следует скачать код openjdk, возьмем последнюю вышедшую версию на момент написания этого поста - 1.8.0_45-b14
hg clone http://hg.openjdk.java.net/jdk8u/jdk8u
cd jdk8u
hg checkout jdk8u45-b14
sh get_source.sh
sh configure --prefix=`pwd`/../jdk8-bin --enable-debug-symbols --with-debug-level=slowdebug --enable-precompiled-headers --disable-zip-debug-info
#для сборки jvm make не поддерживает опцию -j и информирует что необходимо использовать JOBS=4
make
make install
Далее нам понадобится отладчик c++ кода. Разумеется, можно использовать gdb напрямую, но в этой статье я приведу пример с использвоанием eclipse и CDT. Это избавит нас от введения очень длинных команд в дебаггер (подключение shared библиотек). Вам необходимо создать новый проект импортировав директорию с кодом jdk. В настройках проекта укажите путь заголовочным файлам (Properties->C/C++ General->Paths and Symbols->Includes).
К сожалению, не существует единого правила как найти необходимое место в коде hotspot. Это почти всегда исследование. В особо сложных случаях, можно прибегнуть к почтовым рассылкам разработчиков openjdk. Часто помогает включение имеющихся диагностических опций, которые выводят дополнительную отладочную информацию. Смотри список всех опций с описанием.
java -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -XX:+PrintFlagsWithComments
Обратите внимание, что часть из имеющихся опций работают только в jvm собранной для отладки.
Давайте проследим возможный ход поиска конкретного места в коде jvm отвечающего за оптимизацию циклов. Попробуем воспользоваться опцией -XX:+PrintOpto включающей отладочный вывод работы JIT компилятора на уровне C2. Для исследования поведения конкретного участка Java кода, рекомендуется вынести его в отдельный класс и оставить только интересующие строки. Убедится в том, что выявленные ранее оптимизации всё еще применяются к этому коду, можно сравнив дизассемблированный код. Ниже, пример подобного класса для функции оперирующей int значениями.
public class Test {

    public static void main(String[] args) {
        Test t = new Test();
        while (true) {
            t.calculateWithInt(RANDOM_INT);
        }
    }

    private static int RANDOM_INT = (int) Math.random();
    
    public int calculateWithInt(int randomNumber) {
        int counter = 0;
        for (int i = 0; i < 10000; i++) {
          counter += randomNumber;
        }
        return counter;
      }
}
К сожалению, на моем окружении включение опции VerifyLoopOptimizations приводит к падению процесса jvm. При запуске java -XX:+PrintOpto -XX:+VerifyLoopOptimizations -cp test/target/classes test.Test должна выдаваться отладочная информация показывающая, что применяется оптимизация unroll для цикла.
Для идентификации участка исходного кода отвечающего за unroll циклов, используем простой текстовый поиск по исходному коду. Один из найденных файлов это hotspot/src/share/vm/opto/loopTransform.cpp. Если вы абсолютно не знакомы с организацией кода hotspot, то первые разы у вас будет уходить большое количество времени на определение нужного файла. Часто в этом помогает вывод отладочной информации по которой потом может быть сделан текстовый поиск. Но не в этот раз (см. причину выше. На самом деле был еще способ, но об этом ниже). Напомню, что мы используем следующий коммит 1368:15b679d327da помеченный тэгом jdk8u45-b14.

Давайте запустим отладчик и посмотрим, что происходит в методе с сигнатурой
void PhaseIdealLoop::do_unroll( IdealLoopTree *loop, Node_List &old_new, bool adjust_min_trip )
Практически вначале метода мы видим условия для вывода отладочной информации:
  if (PrintOpto && VerifyLoopOptimizations) {
    tty->print("Unrolling ");
    loop->dump_head();
  } else if (TraceLoopOpts) {
    if (loop_head->trip_count() < (uint)LoopUnrollLimit) {
      tty->print("Unroll %d(%2d) ", loop_head->unrolled_count()*2, loop_head->trip_count());
    } else {
      tty->print("Unroll %d     ", loop_head->unrolled_count()*2);
    }
    loop->dump_head();
  }
Рассмотрим второй вариант с проверкой опции TraceLoopOpts, оказывается мы не были ограничены использованием PrintOpto && VerifyLoopOptimizations для вывода отладочной информации об оптимизациях циклов. К счастью, TraceLoopOpts не приводит к краху процесса jvm и выводит несколько больший объем информации. Это конечно же не лучший путь к изучению возможных опций, но зато мы четко видим где это используется и понимаем отладочный вывод существенно глубже. Запустим тестовый класс с этой опцией и посмотрим на вывод.
java -XX:+TraceLoopOpts -cp test/target/classes test.Test
Counted        Loop: N107/N95  limit_check predicated counted [int,10000),+1 (-1 iters) 
Loop: N0/N0 
  Loop: N107/N95  limit_check predicated counted [int,10000),+1 (-1 iters) 
PeelMainPost   Loop: N107/N95  limit_check predicated counted [int,10000),+1 (10034 iters) 
Unroll 2       Loop: N107/N95  counted [int,10000),+1 (10034 iters)  main
Loop: N0/N0 
  Loop: N133/N135  limit_check predicated counted [int,int),+1 (4 iters)  pre
  Loop: N159/N95  counted [int,9999),+2 (10034 iters)  main
  Loop: N115/N117  counted [int,10000),+1 (4 iters)  post
Unroll 4       Loop: N159/N95  counted [int,9999),+2 (10034 iters)  main
Loop: N0/N0 
  Loop: N133/N135  limit_check predicated counted [int,int),+1 (4 iters)  pre
  Loop: N178/N95  counted [int,9997),+4 (10034 iters)  main
  Loop: N115/N117  counted [int,10000),+1 (4 iters)  post
Unroll 8       Loop: N178/N95  counted [int,9997),+4 (10034 iters)  main
Loop: N0/N0 
  Loop: N133/N135  limit_check predicated counted [int,int),+1 (4 iters)  pre
  Loop: N195/N95  counted [int,9993),+8 (10034 iters)  main
  Loop: N115/N117  counted [int,10000),+1 (4 iters)  post
Unroll 16       Loop: N195/N95  counted [int,9993),+8 (10034 iters)  main
Loop: N0/N0 
  Loop: N133/N135  limit_check predicated counted [int,int),+1 (4 iters)  pre
  Loop: N212/N95  counted [int,9985),+16 (10034 iters)  main
  Loop: N115/N117  counted [int,10000),+1 (4 iters)  post
PredicatesOff
Loop: N0/N0 
  Loop: N133/N135  counted [int,int),+1 (4 iters)  pre
  Loop: N212/N95  counted [int,9985),+16 (10034 iters)  main
  Loop: N115/N117  counted [int,10000),+1 (4 iters)  post
Counted        Loop: N93/N82  limit_check predicated counted [0,10000),+1 (-1 iters) 
Loop: N0/N0 
  Loop: N93/N82  limit_check predicated counted [0,10000),+1 (-1 iters) 
PeelMainPost   Loop: N93/N82  limit_check predicated counted [0,10000),+1 (10058 iters) 
Unroll 2       Loop: N93/N82  counted [int,10000),+1 (10058 iters)  main
Loop: N0/N0 
  Loop: N141/N82  counted [1,9999),+2 (10058 iters)  main
  Loop: N97/N102  counted [int,10000),+1 (4 iters)  post
Unroll 4       Loop: N141/N82  counted [1,9999),+2 (10058 iters)  main
Loop: N0/N0 
  Loop: N161/N82  counted [1,9997),+4 (10058 iters)  main
  Loop: N97/N102  counted [int,10000),+1 (4 iters)  post
Unroll 8       Loop: N161/N82  counted [1,9997),+4 (10058 iters)  main
Loop: N0/N0 
  Loop: N178/N82  counted [1,9993),+8 (10058 iters)  main
  Loop: N97/N102  counted [int,10000),+1 (4 iters)  post
Unroll 16       Loop: N178/N82  counted [1,9993),+8 (10058 iters)  main
Loop: N0/N0 
  Loop: N195/N82  counted [1,9985),+16 (10058 iters)  main
  Loop: N97/N102  counted [int,10000),+1 (4 iters)  post
Counted          Loop: N202/N184  limit_check predicated counted [0,10000),+1 (-1 iters) 
Loop: N0/N0 
  Loop: N195/N189  limit_check predicated
    Loop: N202/N184  limit_check predicated counted [0,10000),+1 (-1 iters) 
Predicate IC   Loop: N195/N189  limit_check predicated
Loop: N0/N0 
  Loop: N195/N189  limit_check predicated
    Loop: N202/N184  limit_check predicated counted [0,10000),+1 (-1 iters) 
Empty with zero trip guard       Loop: N202/N184  limit_check predicated counted [0,10000),+1 (-1 iters) 
Loop: N0/N0 
  Loop: N195/N189  limit_check predicated
Peel           Loop: N195/N189  limit_check predicated
SplitIf
Видно уже знакомые нам цифры N195/N82 counted [1,9985),+16 Диапазон изменения инкремента нашего цикла и шаг этого изменения. Для тех кто забыл, приведу пару строчек из дизассемблированного кода.
0x00007f6be6962af3: add    r8d,0x10
0x00007f6be6962af7: cmp    r8d,0x2701
Метод do_unroll, как и остальной код работающий с исполняемым кодом, оперирует объектами класса Node, представляющим любой блок кода (вспомните блоки рассматриваемые в дизассемблированном коде). У этого класса множество наследников, один из них CountedLoopNode. Нетрудно догадаться, что последний является блоком кода цикла со счетчиком. За один проход - метод сокращает количество итераций цикла в два раза. Давайте попытаемся разобраться почему в случае с int мы имеем сокращение количества итераций в 16 раз, а в случае с Integer только в 2. Рассмотрим верх стека вызовов когда мы находимся в методе do_unroll
PhaseIdealLoop::do_unroll() at loopTransform.cpp:1,196 0x7ffff67ebef5
IdealLoopTree::iteration_split_impl() at loopTransform.cpp:2,371 0x7ffff67f0b0f
IdealLoopTree::iteration_split() at loopTransform.cpp:2,412 0x7ffff67f0c9e
PhaseIdealLoop::build_and_optimize() at loopnode.cpp:2,364 0x7ffff67fd45c
PhaseIdealLoop::PhaseIdealLoop() at loopnode.hpp:789 0x7ffff641e39e
Compile::Optimize() at compile.cpp:2,136 0x7ffff6414c06 
Compile::Compile() at compile.cpp:848 0x7ffff640f398
Взглянем на bool IdealLoopTree::iteration_split_impl( PhaseIdealLoop *phase, Node_List &old_new ).
В 2371 строке вызывается do_unroll если до этого установлена переменная should_unroll, которая в свою очередь устанавливается значением вычисляемым в методе bool IdealLoopTree::policy_unroll( PhaseIdealLoop *phase ). Данный метод производит достаточно большое количество проверок. Но нас интересует всего два участка
CountedLoopNode *cl = _head->as_CountedLoop();
//...
int future_unroll_ct = cl->unrolled_count() * 2;
if (future_unroll_ct > LoopMaxUnroll) return false;
//...
// Check for being too big
if (body_size > (uint)LoopUnrollLimit) {
  if (xors_in_loop >= 4 && body_size < (uint)LoopUnrollLimit*4) return true;
  // Normal case: loop too big
  return false;
}
//...
Эти два участка содержат проверки не превышено ли максимальное количество допустимых unroll операций и не превышает ли размер тела цикла максимально допустимое значение. Заметьте, под телом цикла подразумевается все копии блоков созданных в результате предыдущих unroll операций. Ниже представлены набор параметров регулирующих данное поведение. С их помощью можно изменять поведения в продакшен сборке hotspot.
intx LoopMaxUnroll = 16 {C2 product}Maximum number of unrolls for main loop
intx LoopOptsCount = 43 {C2 product}Set level of loop optimization for tier 1 compiles
intx LoopUnrollLimit = 60 {C2 pd product}Unroll loop bodies with node count less than this
intx LoopUnrollMin = 4 {C2 product}Minimum number of unroll loop bodies before checking progressof rounds of unroll,optimize,..
Напоследок я приготовил пару задачек для читателя который хочет поэксперементировать самостоятельно.
  1. Подберите значение вышеописанных параметров для того, чтобы количество итераций циклов в исследуемых тестовых методах совпадало во время выполнения (после применения всех оптимизаций).
  2. Выясните, что влияет на размер тела цикла в случае тестового примера над Integer, если известно, что простой тестовый класс с соответствующим методом (подобно классу Test с методом для int) при выполнении оптимизируется до количества операций 10000/16 (unroll 16).
PS В коде теста присутвует проблема. Необходимо использовать JMH blackhole для того, чтобы предотвратить оптимизацию вызова функции значение которой нигде не используется. Спасибо Александру Самолову.

Дополнительные источники


Комментариев нет:

Отправить комментарий