Введение Наконец я дошел до подробного изучения байт-кода 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
-
Проксетерий: Глава 1. Часть 1: Введение
19 Oct, 24 -
Шлюз Jabber-Evernote
19 Oct, 24 -
Html-Разметка В Linux
19 Oct, 24 -
Идеологии Спора. Истинный.
19 Oct, 24 -
Игры, В Которые Запрещено Играть
19 Oct, 24