Для чего 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 массива в указанное место нативной памяти;Пример GetByteArrayRegionJNIEXPORT 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.Пример GetByteArrayElementsJNIEXPORT 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
.Пример GetPrimitiveArrayCriticalJNIEXPORT 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(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 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.
Автор: apangin