- PVSM.RU - https://www.pvsm.ru -
Для чего Java-программисты прибегают к native методам? Иногда, чтобы воспользоваться сторонней DLL библиотекой. В других случаях, чтобы ускорить критичный алгоритм за счет оптимизированного кода на C или ассемблере. Например, для обработки потокового медиа, для сжатия, шифрования и т.п.
Но вызов native метода не бесплатен. Порой, накладные расходы на JNI оказываются даже больше, чем выигрыш в производительности. А всё потому, что они включают в себя:
jobject
);JNIEnv*
и jclass
;synchronized
;in_Java
в in_native
и обратно;Но зачастую native методы просты: они не бросают исключений, не создают новые объекты в хипе, не обходят стек, не работают с хендлами и не синхронизованы. Можно ли для них не делать лишних действий?
Да, и сегодня я расскажу о недокументированных возможностях HotSpot JVM для ускоренного вызова простых JNI методов. Хотя эта оптимизация появилась еще с первых версий Java 7, что удивительно, о ней еще никто нигде не писал.
Рассмотрим для примера простой native метод, получающий на вход массив byte[]
и возвращающий сумму элементов. Есть несколько способов работы с массивом в JNI:
GetByteArrayRegion
– копирует элементы Java массива в указанное место нативной памяти;
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.
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;
}
GetPrimitiveArrayCritical
, которая возвращает прямой адрес массива в хипе, но при этом запрещает работу GC до вызова ReleasePrimitiveArrayCritical
.
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;
}
А вот и наш секретный инструмент. Внешне он похож на обычный 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 Natives задумывался как приватный API Хотспота для JDK, чтобы ускорить вызов криптографических функций, реализованных в нативе. Максимум, что можно найти из описания – комментарии к задаче в багтрекере [2]. Важная особенность: JavaCritical_
функции вызываются только из горячего (скомилированного) кода, поэтому помимо JavaCritical_
реализации у метода должна быть еще и «запасная» традиционная JNI реализация. Впрочем, для совместимости с другими JVM так даже лучше.
Давайте, измерим, какова же экономия на массивах разной длины: 16, 256, 4KB, 64KB и 1MB. Естественно, с помощью JMH [3].
@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
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/59995
Ссылки в тексте:
[1] ABI: http://en.wikipedia.org/wiki/Application_binary_interface
[2] комментарии к задаче в багтрекере: https://bugs.openjdk.java.net/browse/JDK-7013347
[3] JMH: http://openjdk.java.net/projects/code-tools/jmh/
[4] Источник: http://habrahabr.ru/post/222997/
Нажмите здесь для печати.