Дорог ли native метод? Неизведанная сторона JNI

от автора


Для чего Java-программисты прибегают к native методам? Иногда, чтобы воспользоваться сторонней DLL библиотекой. В других случаях, чтобы ускорить критичный алгоритм за счет оптимизированного кода на C или ассемблере. Например, для обработки потокового медиа, для сжатия, шифрования и т.п.

Но вызов native метода не бесплатен. Порой, накладные расходы на JNI оказываются даже больше, чем выигрыш в производительности. А всё потому, что они включают в себя:

  1. создание stack frame;
  2. перекладывание аргументов в соответствии с ABI;
  3. оборачивание ссылок в JNI хендлы (jobject);
  4. передачу дополнительных аргументов JNIEnv* и jclass;
  5. захват и освобождение монитора, если метод synchronized;
  6. «ленивую» линковку нативной функции;
  7. трассировку входа и выхода из метода;
  8. перевод потока из состояния in_Java в in_native и обратно;
  9. проверку необходимости safepoint;
  10. обработку возможных исключений.

Но зачастую 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(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.

ссылка на оригинал статьи http://habrahabr.ru/post/222997/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *