JavaRush/Java блог/Архив info.javarush/Boxing/Unboxing в цикле
LuisBoroda
28 уровень

Boxing/Unboxing в цикле

Статья из группы Архив info.javarush
участников
При изучении основ Java я частенько сталкивался с различными рекомендациями по использованию типов данных. В один прекрасный день мне захотелось чуть глубже разобраться и узнать, что же на самом деле происходит под капотом, а не просто запомнить, как правильно. Я не стал разбирать всем приевшийся пример с конкатенацией строк и решил разобрать не менее приевшийся пример с авто-упаковкой/распаковкой типов). Для начала стоит сказать пару слов о JVM. JVM является стековой машиной. Грубо говоря, каждый раз, когда мы что-то делаем с переменными – они сначала помещаются в стек операндов, который формируется для каждого метода, а затем производятся необходимые вычисления. Не вдаваясь в детали выглядит это так: Boxing/Unboxing в цикле - 1 Итак, предположим, что у нас есть задача по вычислению арифметической прогрессии от 0 до 1 000 000 000 с шагом 1.

Случай 1

Предположим, что мы написали такой код: public class Test { public static void main(String[] args) { Long result = 0L; for (int i = 0; i < 1_000_000_000; i++) { result += i; } System.out.println("Result:" + result); } } Будет ли он выполнять поставленную задачу? Да, но будет ли он оптимальным? Разберёмся. Для начала давайте посмотрим на байт-код, который получился (его можно получить так: javap –c Test.class). Вот в такую ужасающую конструкцию преобразился наш цикл for: 10: if_icmpge 30 13: aload_1 14: invokevirtual #4 // Method java/lang/Long.longValue:()J 17: iload_2 18: i2l 19: ladd 20: invokestatic #2 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 23: astore_1 24: iinc 2, 1 27: goto 7 Давайте по порядку:
  • 13 – Помещаем переменную Long result в стек операндов
  • 14 – Распаковываем ссылочный Long в примитивный long
  • 17 – Помещаем в стек операндов переменную int i
  • 18 – преобразуем её к типу long
  • 19 – теперь складываем long result и long i
  • 20 – запаковываем результат в ссылочный тип Long
  • 23 – и сохраняем обратно в переменную.
Таким образом, в нашем цикле упаковка и распаковка выполнятся, всего-то, по миллиарду раз. Помимо этого, тип Long является immutable, а это значит, что после каждого вызова метода упаковки Long.valueOf() (строка №20 байт-кода) мы получаем новый объект, при этом старый объект остаётся в памяти и ждёт, когда за ним придёт GC. В итоге, к концу работы программы, сборщик мусора на моей машине срабатывал в среднем 46 раз.

Случай 2

Попробуем заменить тип переменной result на примитивный long: public class Test { public static void main(String[] args) { long result = 0; for (int i = 0; i < 1_000_000_000; i++) { result += i; } System.out.println("Result:" + result); } } Посмотрим, что получилось. Байт-кода стало меньше. 7: if_icmpge 21 10: lload_1 11: iload_3 12: i2l 13: ladd 14: lstore_1 15: iinc 3, 1 18: goto 4
  • 10 – Помещаем в стек операндов нашу переменную result
  • 11 – Помещаем в стек операндов переменную int i
  • 12 – преобразуем int i в long i
  • 13 – складываем long result и long i
  • 14 – сохраняем результат обратно в локальную переменную
Сразу можно заметить, что мы избавились от ненужных операций распаковки/упаковки. А ещё, используя примитивные типы, мы не заставляем память хранить кучу ненужных объектов. Теперь давайте посмотрим на время выполнения обоих примеров:
  • Случай 1 – в среднем 5100 мс
  • Случай 2 – в среднем 535 мс
Почти 10-кратный прирост по скорости. Таким образом, если в коде используются классы-обёртки и с ними иногда необходимо проводить некоторые арифметические операции, то лучше производить распаковку заранее, не полагаясь на то, что JVM всё за вас оптимизирует, как я раньше и считал. P.S. Заметку писал для себя, но вдруг кому-нибудь окажется полезной. И, конечно, замечания, критика и дополнения приветствуются.
Комментарии (4)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
maks1m
Уровень 25
24 сентября 2015, 17:21
Вроде как все логично.
Еще нужно таким образом проверить правда ли что компилятор оптимизирует конкатенацию строк через StringBuilder. Интересно или описанный в статье пример можно принудительно оптимизировать через флаг jvm.
LuisBoroda
Уровень 28
26 сентября 2015, 04:10
Так и есть, всё логично, но хотелось немного глубже понять и посмотреть, как работает).

По поводу конкатенации, из личных наблюдений могу сказать следующее:
Если мы напишем так:
String s = "a" + "b" + "c";

То после компиляции будет строка «abc», поэтому когда мы переносим длинную строку в IDE с помощью оператора "+", то результат будет такой-же, как если бы мы и не использовали конкатенацию вовсе. Так сказать синтаксический сахар.

Но если будет такой код:
String b = "b";
String s = "a" + b + "c";

То будет создан StringBuilder, 3 раза будет вызван метод append(), и один раз toString().
По поводу флагов JVM, к сожалению, не могу дать пояснений. Из тех, что я пробовал использовать — разницы в скорости я не заметил, а на байт-код они, естественно, не влияют. Нужно читать JVMS, а пока для меня работа JVM больше похожа на магию. Так что, если есть идеи, как оптимизировать первый пример с помощью флагов — буду только рад).
EvIv
Уровень 30
23 сентября 2015, 13:42
Отличная иллюстрация "дырявых абстракций" и того, что заглянуть под капот бывает очень полезно и познавательно!
Только пугает, что этих «капотов», одного под другим, сейчас существует десятки и сотни (если еще и энтерпрайз, с его спрингами и хайбернейтами рассматривать) и не под силу человеческому мозгу охватить их все. Эх, где времена бейсика и асма =))))))
LuisBoroda
Уровень 28
24 сентября 2015, 13:22
Вот именно по этому от джунов и требуют хорошее знание и понимание Java Core, а Spring с Hibernat'ом дело наживное).
Хорошая статья, спасибо, вот эта строчка хорошо отражает суть:
абстракции экономят наше рабочее время, но не экономят учебное время.
Хотя, я думаю, что не всегда необходимо знать, что там под капотом, не зря же инкапсуляцию придумали) Банальный жизненный пример — чтобы использовать в своей работе монитор необязательно знать принцип его работы, достаточно знать, как его подключить и следовать рекомендациям по использованию (не заливать водой, например), так и с некоторыми абстракциями). Тем не менее документацию всё равно читать нужно.