Дорог ли native метод? «Секретное» расширение JNI
Для чего Java-программисты прибегают к native методам? Иногда, чтобы воспользоваться сторонней DLL библиотекой. В других случаях, чтобы ускорить критичный алгоритм за счет оптимизированного кода на C или ассемблере. Например, для обработки потокового медиа, для сжатия, шифрования и т.п.Но вызов native метода не бесплатен. Порой, накладные расходы на JNI оказываются даже больше, чем выигрыш в производительности. А всё потому, что они включают в себя:
создание stack frame; перекладывание аргументов в соответствии с ABI; оборачивание ссылок в JNI хендлы (jobject); передачу дополнительных аргументов JNIEnv* и jclass; захват и освобождение монитора, если метод synchronized; «ленивую» линковку нативной функции; трассировку входа и выхода из метода; перевод потока из состояния in_Java в in_native и обратно; проверку необходимости safepoint; обработку возможных исключений. Но зачастую native методы просты: они не бросают исключений, не создают новые объекты в хипе, не обходят стек, не работают с хендлами и не синхронизованы. Можно ли для них не делать лишних действий? Да, и сегодня я расскажу о недокументированных возможностях HotSpot JVM для ускоренного вызова простых JNI методов. Хотя эта оптимизация появилась еще с первых версий Java 7, что удивительно, о ней еще никто нигде не писал.
JNI, каким мы его знаем Рассмотрим для примера простой native метод, получающий на вход массив byte[] и возвращающий сумму элементов. Есть несколько способов работы с массивом в JNI: GetByteArrayRegion — копирует элементы Java массива в указанное место нативной памяти; Пример GetByteArrayRegion JNIEXPORT jint JNICALL Java_bench_Natives_arrayRegionImpl (JNIEnv* env, jclass cls, jbyteArray array) { static jbyte buf[1048576]; jint length = (*env)→GetArrayLength (env, array); (*env)→GetByteArrayRegion (env, array, 0, length, buf); return sum (buf, length); } GetByteArrayElements — то же самое, только JVM сама выделяет область памяти, куда будут скопированы элементы. По окончании работы с массивом необходимо вызвать ReleaseByteArrayElements.Пример GetByteArrayElements JNIEXPORT jint JNICALL Java_bench_Natives_arrayElementsImpl (JNIEnv* env, jclass cls, jbyteArray array) { jboolean isCopy; jint length = (*env)→GetArrayLength (env, array); jbyte* buf = (*env)→GetByteArrayElements (env, array, &isCopy); jint result = sum (buf, length); (*env)→ReleaseByteArrayElements (env, array, buf, JNI_ABORT); return result; } Зачем, спросите вы, делать копию массива? Но ведь работать с объектами в Java Heap напрямую из натива нельзя, так как они могут перемещаться сборщиком мусора прямо во время работы JNI метода. Однако есть функция GetPrimitiveArrayCritical, которая возвращает прямой адрес массива в хипе, но при этом запрещает работу GC до вызова ReleasePrimitiveArrayCritical.Пример GetPrimitiveArrayCritical JNIEXPORT jint JNICALL Java_bench_Natives_arrayElementsCriticalImpl (JNIEnv* env, jclass cls, jbyteArray array) { jboolean isCopy; jint length = (*env)→GetArrayLength (env, array); jbyte* buf = (jbyte*) (*env)→GetPrimitiveArrayCritical (env, array, &isCopy); jint result = sum (buf, length); (*env)→ReleasePrimitiveArrayCritical (env, array, buf, JNI_ABORT); return result; } Critical Native А вот и наш секретный инструмент. Внешне он похож на обычный JNI метод, только с приставкой JavaCritical_ вместо Java_. Среди аргументов отсутствуют JNIEnv* и jclass, а вместо jbyteArray передаются два аргумента: jint length — длина массива и jbyte* data — «сырой» указатель на элементы массива. Таким образом, Critical Native методу не нужно вызывать дорогие JNI функции GetArrayLength и GetByteArrayElements — можно сразу работать с массивом. На время выполнения такого метода GC будет отложен. JNIEXPORT jint JNICALL JavaCritical_bench_Natives_javaCriticalImpl (jint length, jbyte* buf) { return sum (buf, length); } Как видим, в реализации не осталось ничего лишнего.Но чтобы метод мог стать Critical Native, он должен удовлетворять строгим ограничениям: метод должен быть static и не synchronized; среди аргументов поддерживаются только примитивные типы и массивы примитивов; Critical Native не может вызывать JNI функции, а, следовательно, аллоцировать Java объекты или кидать исключения; и, самое главное, метод должен завершаться за короткое время, поскольку на время выполнения он блокирует GC. Critical Natives задумывался как приватный API Хотспота для JDK, чтобы ускорить вызов криптографических функций, реализованных в нативе. Максимум, что можно найти из описания — комментарии к задаче в багтрекере. Важная особенность: JavaCritical_ функции вызываются только из горячего (скомилированного) кода, поэтому помимо JavaCritical_ реализации у метода должна быть еще и «запасная» традиционная JNI реализация. Впрочем, для совместимости с другими JVM так даже лучше.Сколько будет в граммах? Давайте, измерим, какова же экономия на массивах разной длины: 16, 256, 4KB, 64KB и 1MB. Естественно, с помощью JMH.Бенчмарк @State (Scope.Benchmark) public class Natives {
@Param ({»16»,»256»,»4096»,»65536»,»1048576»}) int length; byte[] array;
@Setup public void setup () { array = new byte[length]; }
@GenerateMicroBenchmark public int arrayRegion () { return arrayRegionImpl (array); }
@GenerateMicroBenchmark public int arrayElements () { return arrayElementsImpl (array); }
@GenerateMicroBenchmark public int arrayElementsCritical () { return arrayElementsCriticalImpl (array); }
@GenerateMicroBenchmark public int javaCritical () { return javaCriticalImpl (array); }
static native int arrayRegionImpl (byte[] array); static native int arrayElementsImpl (byte[] array); static native int arrayElementsCriticalImpl (byte[] array); static native int javaCriticalImpl (byte[] array);
static { System.loadLibrary («natives»); } } Результаты Java™ SE Runtime Environment (build 1.7.0_51-b13) Java HotSpot™ 64-Bit Server VM (build 24.51-b03, mixed mode)
Benchmark (length) Mode Samples Mean Mean error Units b.Natives.arrayElements 16 thrpt 5 7001,853 66,532 ops/ms b.Natives.arrayElements 256 thrpt 5 4151,384 89,509 ops/ms b.Natives.arrayElements 4096 thrpt 5 571,006 5,534 ops/ms b.Natives.arrayElements 65536 thrpt 5 37,745 2,814 ops/ms b.Natives.arrayElements 1048576 thrpt 5 1,462 0,017 ops/ms b.Natives.arrayElementsCritical 16 thrpt 5 14467,389 70,073 ops/ms b.Natives.arrayElementsCritical 256 thrpt 5 6088,534 218,885 ops/ms b.Natives.arrayElementsCritical 4096 thrpt 5 677,528 12,340 ops/ms b.Natives.arrayElementsCritical 65536 thrpt 5 44,484 0,914 ops/ms b.Natives.arrayElementsCritical 1048576 thrpt 5 2,788 0,020 ops/ms b.Natives.arrayRegion 16 thrpt 5 19057,185 268,072 ops/ms b.Natives.arrayRegion 256 thrpt 5 6722,180 46,057 ops/ms b.Natives.arrayRegion 4096 thrpt 5 612,198 5,555 ops/ms b.Natives.arrayRegion 65536 thrpt 5 37,488 0,981 ops/ms b.Natives.arrayRegion 1048576 thrpt 5 2,054 0,071 ops/ms b.Natives.javaCritical 16 thrpt 5 60779,676 234,483 ops/ms b.Natives.javaCritical 256 thrpt 5 9531,828 67,106 ops/ms b.Natives.javaCritical 4096 thrpt 5 707,566 13,330 ops/ms b.Natives.javaCritical 65536 thrpt 5 44,653 0,927 ops/ms b.Natives.javaCritical 1048576 thrpt 5 2,793 0,047 ops/ms Оказывается, для маленьких массивов стоимость JNI вызова в разы превосходит время работы самого метода! Для массивов в сотни байт накладные расходы сравнимы с полезной работой. Ну, а для многокилобайтных массивов способ вызова не столь важен — всё время тратится собственно на обработку.Выводы Critical Natives — приватное расширение JNI в HotSpot, появившееся с JDK 7. Реализовав JNI-подобную функцию по определенным правилам, можно значительно сократить накладные расходы на вызов native метода и обработку Java-массивов в нативном коде. Однако для долгоиграющих функций такое решение не подойдет, поскольку GC не сможет запуститься, пока исполняется Critical Native.