В ответ на вопрос почему доступ к элементу массива быстрее чем доступ к полям объекта

в 19:30, , рубрики: hotspot, java, performance, метки: ,

Совсем недавно уважаемый lany написал замечательный пост про mutable numbers в Java: http://habrahabr.ru/post/151887/
В комментарии к его посту я упомянул, что если важна производительность, то можно заменить объект-обертку на одно-элементный массив, доступ к элементу массива по определению быстрее чем извлечение значения из instance field.
Этот стереотип мне достался в наследство от Sun после прочтения очередного performance white paper. Там было написано что быстрее всего происходит доступ к локальной переменной, следом идет статический филд, потом элемент массива и замыкает этот список instance field.
К счастью на слово мне не поверили и это послужило поводом для написания этой статьи.
Статья не расчитана на джуниоров, читатель должен знать Java, ASM x86 и байткод.


Для того чтобы понять быстрее будет или нет доступ к элементу массива в сравнении с доступом к полю объекта можно:

  1. Написать синтетический performance test
  2. Разобраться что стоит на самом деле за 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js