Pull to refresh

Дорог ли native метод? «Секретное» расширение JNI

Reading time 5 min
Views 32K

Для чего 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.
Tags:
Hubs:
+46
Comments 23
Comments Comments 23

Articles