Поиск Причин Странной Производительности



Введение Наконец я дошел до подробного изучения байт-кода Java, и почти сразу в голове возник интересный вопрос.

Там есть инструкции НЕТ , который ничего не делает. Итак, как это «ничего» влияет на производительность? Собственно процесс изучения этого описан в посте.



Отказ от ответственности

Сама история, в первую очередь, не о том, как это на самом деле работает, а о том, каких ошибок следует опасаться при измерении производительности.



Инструменты

Начнем с главного: как проводились все измерения.

Библиотека использовалась для генерации кода КАК М.

, чтобы создать сам тест - ДМХ .

Чтобы избежать использования отражения, был создан небольшой интерфейс:

  
  
  
  
  
  
  
  
  
   

public interface Getter { int get(); }

Далее был сгенерирован класс, реализующий метод получать :

public get()I NOP .

NOP LDC 20 IRETURN

Вы можете вставить произвольное количество узлов.

Полный код генератора

public class SimpleGetterClassLoader extends ClassLoader { private static final String GENERATED_CLASS_NAME = "other.GeneratedClass"; private static final ClassLoader myClassLoader = new SimpleGetterClassLoader(); @SuppressWarnings("unchecked") public static Getter newInstanceWithNOPs(int nopCount) throws Exception { Class<?> clazz = Class.forName(GENERATED_CLASS_NAME + "_" + nopCount, false, myClassLoader); return (Getter) clazz.newInstance(); } @NotNull @Override protected Class<?> findClass(@NotNull String name) throws ClassNotFoundException { if (!name.startsWith(GENERATED_CLASS_NAME)) throw new ClassNotFoundException(name); int nopCount = Integer.parseInt(name.substring(GENERATED_CLASS_NAME.length() + 1)); ClassWriter cw = new ClassWriter(0); cw.visit(V1_5, ACC_PUBLIC, name.replace('.

', '/'), null, getInternalName(Object.class), new String[]{getInternalName(Getter.class)}); { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, getInternalName(Object.class), "<init>", "()V"); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "get", "()I", null, null); mv.visitCode(); for (int i = 0; i < nopCount; i++) { mv.visitInsn(NOP); } mv.visitLdcInsn(20); mv.visitInsn(IRETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } cw.visitEnd(); byte[] bytes = cw.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } }

Контрольный показатель

@State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MICROSECONDS) public class Bench { private Getter nop_0; private Getter nop_10; .

@Setup public void setup() throws Exception { nop_0 = newInstanceWithNOPs(0); nop_10 = newInstanceWithNOPs(10); .

} @GenerateMicroBenchmark public int nop_0() { return nop_0.get(); } @GenerateMicroBenchmark public int nop_10() { return nop_10.get(); } .





Поиск истины

Сначала было запущено 2 теста: без нопов и с 2000.

Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 838,753 48,962 ops/us b.Bench.nop_2000 thrpt 5 298,428 7,965 ops/us

И сразу сделал очень весомый вывод: «Тупой JIT не вырезает шаги, а переводит их в машинные».

Вопрос к знатокам: Если бы это было правдой, были бы результаты измерений аналогичными? Или это будет что-то совершенно другое? Но это была еще гипотеза, и мне очень хотелось ее проверить.

Сначала я убедился, что эти методы действительно компилируются JIT, потом посмотрел, что именно.

Естественно, получившийся ассемблер оказался полностью идентичным.

И тут я понял, что чего-то не понимаю.

Исполняемый код полностью идентичен, а производительность отличается в 2,5 раза.

Странный.

Далее мне очень хотелось посмотреть на тип зависимости.



Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 813,010 71,510 ops/us b.Bench.nop_2000 thrpt 5 302,589 12,360 ops/us b.Bench.nop_10000 thrpt 5 0,268 0,017 ops/us

Скрытые знания Это измерение просто великолепно.

3 пункта, все из разных последовательностей.

Стоит отдельно отметить, что по новому пункту я не смотрел, происходит ли компиляция или что получается на выходе.

Автоматически предполагал, что все так же, как и на 0/2к.

Это было ошибкой.

Я посмотрел на это и сделал следующий далеко идущий вывод: «Зависимость очень сильно нелинейная».

Но, что гораздо важнее, именно в этот момент я начал подозревать, что настоящая проблема здесь не в самих ножках, а в размере метода.

Следующей мыслью было то, что наши методы виртуальные, а значит, хранятся в таблице виртуальных методов.

Может быть, сама таблица чувствительна к размеру? Для проверки я просто перенес код в статические методы, и, естественно, вообще ничего не изменилось.

Вопрос к знатокам 2 «Неужели эта мысль была полной ерундойЭ» Или в ней еще было что-то здоровое? Потом по недоразумению я начал искать при чем здесь размер метода.

Ответ был найден в источниках openjdk:

develop(intx, HugeMethodLimit, 8000, \ "Don't compile methods larger than this if " \ "+DontCompileHugeMethods")

Интересно, всего от 2к до 10к.

Давайте посчитаем размер моего метода: 3 байта на «возврат 20», осталось 7997.

Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 797,376 12,998 ops/us b.Bench.nop_2000 thrpt 5 306,795 0,243 ops/us b.Bench.nop_7997 thrpt 5 303,314 7,161 ops/us b.Bench.nop_7998 thrpt 5 0,335 0,001 ops/us b.Bench.nop_10000 thrpt 5 0,269 0,000 ops/us

Как вы уже догадались, эта граница чёткая.

Осталось понять, что происходит до 8000 байт. Добавим баллы:

Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 853,499 61,847 ops/us b.Bench.nop_10 thrpt 5 845,861 112,504 ops/us b.Bench.nop_100 thrpt 5 867,068 20,681 ops/us b.Bench.nop_500 thrpt 5 304,116 1,665 ops/us b.Bench.nop_1000 thrpt 5 299,295 8,745 ops/us b.Bench.nop_2000 thrpt 5 306,495 0,578 ops/us b.Bench.nop_7997 thrpt 5 301,322 7,992 ops/us b.Bench.nop_7998 thrpt 5 0,335 0,005 ops/us b.Bench.nop_10000 thrpt 5 0,269 0,004 ops/us b.Bench.nop_25000 thrpt 5 0,105 0,007 ops/us b.Bench.nop_50000 thrpt 5 0,053 0,001 ops/us

Первое, что нас здесь радует, это то, что после отключения jit очень четко видна линейная зависимость.

Что в точности совпадает с нашими ожиданиями, потому что.

каждый NOP должен быть явно обработан.

Следующее, на что бросается взгляд, это стойкое ощущение, что до 8к существует не одна зависимость, а просто 2 константы.

Еще 5 минут ручного бинарного поиска и граница найдена.



Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 805,466 10,074 ops/us b.Bench.nop_10 thrpt 5 862,027 4,756 ops/us b.Bench.nop_100 thrpt 5 861,462 9,881 ops/us b.Bench.nop_322 thrpt 5 863,176 22,385 ops/us b.Bench.nop_323 thrpt 5 303,677 5,130 ops/us b.Bench.nop_500 thrpt 5 299,368 11,143 ops/us b.Bench.nop_1000 thrpt 5 302,884 3,373 ops/us b.Bench.nop_2000 thrpt 5 306,682 3,598 ops/us b.Bench.nop_7997 thrpt 5 301,457 4,209 ops/us b.Bench.nop_7998 thrpt 5 0,337 0,001 ops/us b.Bench.nop_10000 thrpt 5 0,268 0,004 ops/us b.Bench.nop_25000 thrpt 5 0,107 0,002 ops/us b.Bench.nop_50000 thrpt 5 0,053 0,000 ops/us

Почти всё, осталось понять, что это за граница.

Давайте посчитаем: 3+322==325. Ищем, что это за волшебное 325, и находим определенный ключ -XX:FreqInlineSize

FreqInlineSize равен 325 в современном 64-битном Linux.
и его описание из документации:
Целое число, указывающее максимальное количество инструкций байт-кода в часто выполняемом методе, который встраивается.

Ура! Наконец все сошлось.

Итого мы обнаружили зависимость производительности от размера метода (конечно, «при прочих равных»).

1. JIT + встроенный 2. Точный срок 3. честная интерпретация

Заключение

Как я сказал вначале, главное, на что следует обращать внимание, — это не реальное поведение.

Это оказалось довольно тривиально, и я уверен, что это описано в доке (не читал, не знаю).

Мой основной посыл заключается в том, что очень важно доверять здравому смыслу, и если результаты измерений хоть немного расходятся с ним, или просто кажутся непонятными, то все необходимо проверять и перепроверять.

Надеюсь, кому-то этот пост показался интересным.



P.S.

Я продолжал считать и 8000, и 325 в байтах включительно.

Похоже это надо было сделать в инструкции невключительно.

Вопрос к знатокам 3 Почему именно 325 и 8000? Это случайные числа или за ними что-то стоит? Теги: #java #производительность #размер метода #размер метода #nop #программирование #java

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.