Совсем недавно уважаемый lany написал замечательный пост про mutable numbers в Java: http://habrahabr.ru/post/151887/
В комментарии к его посту я упомянул, что если важна производительность, то можно заменить объект-обертку на одно-элементный массив, доступ к элементу массива по определению быстрее чем извлечение значения из instance field.
Этот стереотип мне достался в наследство от Sun после прочтения очередного performance white paper. Там было написано что быстрее всего происходит доступ к локальной переменной, следом идет статический филд, потом элемент массива и замыкает этот список instance field.
К счастью на слово мне не поверили и это послужило поводом для написания этой статьи.
Статья не расчитана на джуниоров, читатель должен знать Java, ASM x86 и байткод.
Для того чтобы понять быстрее будет или нет доступ к элементу массива в сравнении с доступом к полю объекта можно:
- Написать синтетический performance test
- Разобраться что стоит на самом деле за array[0] и this.value
Я выбрал второй путь, от синтетических тестов уже тошнит. Для начала накидаем тестовый класс.
package numbers;
public class TestNumbers {
public static void main(String[] args) {
test(new IntNumField());
test(IntNumArray.create());
}
private static final void test(int[] mutableInt2) {
for (int index = 10000; index-- > 0;) {
IntNumArray.inc(mutableInt2);
}
}
private static final void test(IntNumField mutableInt1) {
for (int index = 10000; index-- > 0;) {
mutableInt1.inc();
}
}
public static final class IntNumField {
protected int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public final void inc() {
this.value++;
}
}
public static final class IntNumArray {
public static final int[] create() {
return new int[1];
}
public static final int get(int[] array) {
return array[0];
}
public static final void set(int[] array, int value) {
array[0] = value;
}
public static final void inc(int[] array) {
array[0]++;
}
}
}
Класс IntNumField работает с полем объекта, а IntNumArray — как я предложил, с одноэлементным массивом. Каждый из классов содержит метод inc который инкрементит на единицу наш mutable. В методах test прогоняется вызов каждой версии inc 10,000 раз — чтобы метод гарантированно скомпилировался.
Но для начала взглянем на байткод:
~$ javap -c numbers/TestNumbers$IntNumArray
public int getValue();
Code:
0: aload_0
1: getfield #18; //Field value:I
4: ireturn
public void setValue(int);
Code:
0: aload_0
1: iload_1
2: putfield #18; //Field value:I
5: return
public final void inc();
Code:
0: aload_0
1: dup
2: getfield #18; //Field value:I
5: iconst_1
6: iadd
7: putfield #18; //Field value:I
10: return
В методе get все просто, грузим this и кладем в стек значение value. В остальных примерно тоже самое, ну разве что inc использует dup для оптимизации.
Теперь посмотрим что нас ждет в классе IntNumArray:
~$ javap -c numbers/TestNumbers$IntNumArray
public static final int get(int[]);
Code:
0: aload_0
1: iconst_0
2: iaload
3: ireturn
public static final void set(int[], int);
Code:
0: aload_0
1: iconst_0
2: iload_1
3: iastore
4: return
public static final void inc(int[]);
Code:
0: aload_0
1: iconst_0
2: dup2
3: iaload
4: iconst_1
5: iadd
6: iastore
7: return
Примерно тоже самое, разве что getfield заменен на iconst_0 и iaload. Что же, пока все равно непонятно, поэтому взглянем по ассемблер который сгенерирует HotSpot для методов inc с помощью специальных параметров:
~$ java -XX:+UnlockDiagnosticVMOptions -XX:CompileThreshold=10000 -XX:+PrintAssembly numbers.TestNumbers
# {method} 'inc' '()V' in 'numbers/TestNumbers$IntNumField'
# [sp+0x20] (sp of caller)
0x02141a87: cmp 0x4(%ecx),%eax
0x02141a8a: jne 0x020dd100 ; {runtime_call}
[Verified Entry Point]
0x02141a90: mov %eax,0xffffc000(%esp)
0x02141a97: push %ebp
0x02141a98: sub $0x18,%esp ;*aload_0
; - numbers.TestNumbers$IntNumField::inc@0 (line 50)
0x02141a9b: mov 0x8(%ecx),%esi ;*getfield value
; - numbers.TestNumbers$IntNumField::inc@2 (line 50)
0x02141a9e: inc %esi
0x02141a9f: mov %esi,0x8(%ecx) ;*putfield value
; - numbers.TestNumbers$IntNumField::inc@7 (line 50)
0x02141aa2: add $0x18,%esp
0x02141aa5: pop %ebp
0x02141aa6: test %eax,0x1b0100 ; {poll_return}
0x02141aac: ret
# {method} 'inc' '([I)V' in 'numbers/TestNumbers$IntNumArray'
# parm0: ecx = '[I'
# [sp+0x20] (sp of caller)
0x02141c80: mov %eax,0xffffc000(%esp)
0x02141c87: push %ebp
0x02141c88: sub $0x18,%esp ;*aload_0
; - numbers.TestNumbers$IntNumArray::inc@0 (line 71)
0x02141c8b: cmpl $0x0,0x8(%ecx) ; implicit exception: dispatches to 0x02141cb7
0x02141c92: jbe 0x02141cc1
0x02141c98: mov 0xc(%ecx),%esi ;*iaload
; - numbers.TestNumbers$IntNumArray::inc@3 (line 71)
0x02141c9b: inc %esi
0x02141c9c: cmpl $0x0,0x8(%ecx)
0x02141ca3: jbe 0x02141ccd
0x02141ca9: mov %esi,0xc(%ecx) ;*iastore
; - numbers.TestNumbers$IntNumArray::inc@6 (line 71)
0x02141cac: add $0x18,%esp
0x02141caf: pop %ebp
0x02141cb0: test %eax,0x1b0100 ; {poll_return}
0x02141cb6: ret
Мне пришлось урезать оба метода выкинув такие вещи как code alignment и кучу кода в секции [Exception Handler].
Если всмотреться, оба метода используют inc %esi для инкремента значения на стеке. Отличаются только
mov 0x8(%ecx),%esi ;*getfield value
и
cmpl $0x0,0x8(%ecx) ; implicit exception: dispatches to 0x02141cb7
jbe 0x02141cc1
mov 0xc(%ecx),%esi ;*iaload
Первые две строки во втором кусочке кода это проверка того что индекс 0 не выходит за границы массива (0 < array.length). Вот из-за нее и получается что я был неправ, Хуже того, точно такая же проверка происходит двумя строчками ниже… Вряд ли это будет медленнее, но уж точно не будет быстрее чем доступ к полю объекта.
Спасибо apangin за то что усомнился, я думаю это интересно не только нам двоим. Можно спокойно использовать класс-обертку и все прелести ООП.
P.S. Я все-таки запустил performance benchmark, никакой разницы.
Автор: acebanenco