2015-11-21 2 views
4

Я провел несколько тестов во время выполнения с помощью java для циклов и распознал странное поведение. Для моего кода мне нужны объекты-обертки для примитивных типов, таких как int, double и т. Д., Для имитации параметров io и вывода, но это не так. Просто посмотри мой код. Как объекты с доступом к полям быстрее, чем примитивные типы?Оптимизация Java for-loop

for петля с prtimitive типа:

public static void main(String[] args) { 
    double max = 1000; 
    for (int j = 1; j < 8; j++) { 
     double i; 
     max = max * 10; 
     long start = System.nanoTime(); 
     for (i = 0; i < max; i++) { 
     } 
     long end = System.nanoTime(); 
     long microseconds = (end - start)/1000; 
     System.out.println("MicroTime primitive(max: ="+max + "): " + microseconds); 
    } 
} 

Результат:

микропоры примитива (макс: = 10000,0): 110
микропоры примитива (макс: = 100000,0): 1081
микропоры (max: = 1000000,0): 2450
MicroTime-примитив (макс .: = 1.0E7): 28248
MicroTi мне примитивный (макс: = 1.0E8): 276205
микропоры примитив (макс: = 1.0E9): 2729824
микропоры примитив (макс: = 1.0E10): 27547009

for цикл с простым типом (обертка объект):

public static void main(String[] args) { 
    HDouble max = new HDouble(); 
    max.value = 1000; 
    for (int j = 1; j < 8; j++) { 
     HDouble i = new HDouble(); 
     max.value = max.value*10; 
     long start = System.nanoTime(); 
     for (i.value = 0; i.value <max.value; i.value++) { 
     } 
     long end = System.nanoTime(); 
     long microseconds = (end - start)/1000; 
     System.out.println("MicroTime wrapper(max: ="+max.value + "): " + microseconds); 
    } 
} 

Результат:

микропоры обертка (макс: = 10000,0): 157
микропоры Обертка (макс: = 100000,0): 1561
микропоры обертка (макс: = 1000000,0): 3174
микропоры обертка (макс: = 1.0E7): 15630
микропоры обертка (макс: = 1.0E8): 155471
микропоры обертка (макс: = 1.0E9): 1520967
микропоры обертка (макс: = 1.0E10): 15373311

Чем больше итераций, тем быстрее второго кода. Но почему? Я знаю, что java-компилятор и jvm оптимизируют мой код, но я никогда не думал, что примитивные типы могут быть медленнее, чем объекты с доступом к полям.
У кого-нибудь есть правдоподобное объяснение?

Отредактировано: HDouble класс:

public class HDouble { 
    public double value; 

    public HDouble() { 
    } 

    public HDouble(double value) { 
     this.value = value; 
    } 

    @Override 
    public String toString() { 
     return String.valueOf(value); 
    } 
} 

Я также протестировал мою петлю с кодом в нем. Например, я вычисляю сумму -> такое же поведение (разница не такая большая, но я думал, что примитивный алгоритм должен быть намного быстрее?). Сначала я подумал, что расчет длится так долго, что в поле практически нет разницы.

Обертка для цикла:

for (i.value = 0; i.value <max.value; i.value++) { 
    sum.value = sum.value + i.value; 
} 

Результат:

микропоры обертку (макс: = 10000.0): 243
микропоры обертка (макс: = 100000.0): 2805
микропоры обертки (max: = 1000000.0): 3409
микропоры обертка (макс: = 1.0E7): 28104
микропоры обертка (макс: = 1.0E8): 278432
микропоры обертка (макс: = 1.0E9): 2678322
микропоры обертка (макс: = 1.0E10): 26665540

Примитив для цикла:

for (i = 0; i < max; i++) { 
    sum = sum + i; 
} 

Результат:

микропоры примитива (макс: = 10000,0): 149
микропоры примитива (макс: = 100000,0): 1996
микропоры примитива (макс: = 1000000,0): 2289
микропоры примитива (макс: = 1.0E7): 27085
микропоры примитив (макс: = 1.0E8): 279939
микропоры примитив (макс: = 1.0E9): 2759133
микропоры примитив (макс: = 1.0E10): 27369724

+0

Я предлагаю положить какой-то живой код внутри цикла. Пустые петли редки в реальных программах или даже в серьезных тестах, поэтому оптимизация для них может быть не очень последовательной или хорошо протестированной. –

+0

Можете ли вы показать код «HDouble»? – manouti

+0

Я отредактировал мое сообщение и добавил код Hdouble;) – user3490546

ответ

9

Это так легко ведитесь ручные микрообъективы - вы никогда не знаете, что они фактически. Вот почему есть специальные инструменты, такие как JMH. Но давайте проанализируем, что происходит с примитивной ручной работы теста:

static class HDouble { 
    double value; 
} 

public static void main(String[] args) { 
    primitive(); 
    wrapper(); 
} 

public static void primitive() { 
    long start = System.nanoTime(); 
    for (double d = 0; d < 1000000000; d++) { 
    } 
    long end = System.nanoTime(); 
    System.out.printf("Primitive: %.3f s\n", (end - start)/1e9); 
} 

public static void wrapper() { 
    HDouble d = new HDouble(); 
    long start = System.nanoTime(); 
    for (d.value = 0; d.value < 1000000000; d.value++) { 
    } 
    long end = System.nanoTime(); 
    System.out.printf("Wrapper: %.3f s\n", (end - start)/1e9); 
} 

результаты несколько похожи на ваши:

Primitive: 3.618 s 
Wrapper: 1.380 s 

Теперь повторить тест несколько раз:

public static void main(String[] args) { 
    for (int i = 0; i < 5; i++) { 
     primitive(); 
     wrapper(); 
    } 
} 

It становится более интересным:

Primitive: 3.661 s 
Wrapper: 1.382 s 
Primitive: 3.461 s 
Wrapper: 1.380 s 
Primitive: 1.376 s <-- starting from 3rd iteration 
Wrapper: 1.381 s <-- the timings become equal 
Primitive: 1.371 s 
Wrapper: 1.372 s 
Primitive: 1.379 s 
Wrapper: 1.378 s 

Похоже, что оба метода окончательно оптимизированы. Выполнить его еще раз, теперь с активностью компилятора JIT каротаж: -XX:-TieredCompilation -XX:CompileOnly=Test -XX:+PrintCompilation

136 1 %   Test::primitive @ 6 (53 bytes) 
    3725 1 %   Test::primitive @ -2 (53 bytes) made not entrant 
Primitive: 3.589 s 
    3748 2 %   Test::wrapper @ 17 (73 bytes) 
    5122 2 %   Test::wrapper @ -2 (73 bytes) made not entrant 
Wrapper: 1.374 s 
    5122 3    Test::primitive (53 bytes) 
    5124 4 %   Test::primitive @ 6 (53 bytes) 
Primitive: 3.421 s 
    8544 5    Test::wrapper (73 bytes) 
    8547 6 %   Test::wrapper @ 17 (73 bytes) 
Wrapper: 1.378 s 
Primitive: 1.372 s 
Wrapper: 1.375 s 
Primitive: 1.378 s 
Wrapper: 1.373 s 
Primitive: 1.375 s 
Wrapper: 1.378 s 

Примечание % знак в протоколе компиляции на первой итерации. Это означает, что методы были скомпилированы в режиме OSR (on-stack replacement). Во время второй итерации методы перекомпилировались в нормальном режиме. С тех пор, начиная с третьей итерации, не было никакой разницы между примитивом и оберткой в ​​скорости выполнения.

То, что вы на самом деле измерили, - это производительность заглушки OSR. Как правило, это не связано с реальной производительностью приложения, и вам не все равно.

Но вопрос по-прежнему остается, почему OSR-заглушка для обертки скомпилирована лучше, чем для примитивной переменной? Чтобы это выяснить, мы должны получить вниз сгенерированный код сборки:
-XX:CompileOnly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

Я опущу все unrelevant кода оставив только скомпилированную петлю.

Примитивный:

0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1  <-- load double from the stack 
0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm1 
0x00000000023e90de: test %eax,-0x21f90e4(%rip) 
0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp)  <-- store to the stack 
0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0 <-- compare with the stack value 
0x00000000023e90f0: ja  0x00000000023e90d0 

Wrapper:

0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm0 
0x00000000023ebe98: vmovsd %xmm0,0x10(%rbx)  <-- store to the object field 
0x00000000023ebe9d: test %eax,-0x21fbea3(%rip) 
0x00000000023ebea3: vucomisd %xmm0,%xmm1   <-- compare registers 
0x00000000023ebea7: ja  0x00000000023ebe90 

Как вы можете видеть, «примитивный» случай делает ряд нагрузок и магазинов в ячейку стека, а «обертка» делает в основном в -регистрационные операции. Понятно, почему OSR-заглушка относится к стеку: в интерпретируемом режиме локальные переменные хранятся в стеке, а заглушка OSR совместима с этим интерпретированным фреймом. В случае «обертки» значение сохраняется в куче, а ссылка на объект уже кэшируется в регистре.

Смежные вопросы