среда, 12 сентября 2018 г.

Неочевидные оптимизации Jit

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

Есть следующий код:

В методе проиcходит сериализация массива байт. Используется тот факт, что размер char в java - 16 бит, а размер byte (Внезапно 😱) - 8 бит. Но суть не в этом. Интересный момент здесь заключается в том, что метод работает гораздо медленее на массивах нечетной длины.

Очевидно, что большую часть времени выполнение происходит в цикле. Различные профилировщики это подтверждали. В данной ситуации, смотреть байткод смысла было нет, т.к. компилятор в момент своей работы понятия не имеет, какой длины будет передан массив.
Значит применялись какая то оптимизации в рантайме. Это мог только сделать jit компилятор. Для проверки гипотизы, я воспользовался иструментом JitWatcher. Эта программа позволяет посмотреть сгенерированный asm для каждой стадии оптимизации.
Небольшой offtop:
JitWatcher использует параметры jvm для отображения сгенерированного кода. Собственно никто не запрещает это сделать самостоятельно, запустив вашу программу с ключиками:
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+TraceClassLoading -XX:+PrintAssembly.
Но поверьте, вы не захотите в этом разбираться. C JitWatcher вам не придется перелопачивать тонны asm кода, хотя вдруг вам это понравится 😏
Чтобы ключик -XX:+PrintAssembly заработал jvm требует наличие библиотеки hsdis. Она должна находиться в директории $JAVA_HOME/jre/lib/server/. Ее можно либо собрать самостоятельно, либо скачать уже готовый артифакт, например здесь

Но вернемся к нашим баранам. Выяснилось, что jit делает "странное" предположение. Оно заключается в следующем - для массивов четной длины, использовались векторые инструкции, а для работы с массивами нечетной длины он решил их не использовать.

Ясно, что векторная операции это именно то, что может дать существенный прирост производительности. Потребовалось переписать цикл таким образом, чтобы jit использовал их в обоих случаях. Я применил unrolling цикла.

Тут то jit'у было не отвертеться и он стал стабильно компилировать ассемблеровский код с векторными интрукциями независимо от размера массива:

Результаты измерений показывающие разницу:

Benchmark Size of array Mode Score Units
with optimization 100000 avgt 46777.0331 ns/op
with optimization 100001 avgt 47718.458 ns/op
without optimization 100000 avgt 47218.527 ns/op
without optimization 100001 avgt 71312.515 ns/op