![](https://habrastorage.org/getpro/habr/upload_files/990/a48/a0b/990a48a0bdd163f93fab894a5befe90c.jpg)
Вы когда-нибудь задумывались, насколько grpc быстрый. Да, в сети, ему равных нет. Если вы гоняете маленькие сообщения, которые надо быстро доставить, то лучше grpc попросту не найти ( найти, но по мнению автору protobuf остается движком сериализации, поддерживающим большее кол-во языков ). Но насколько он хорош? Сможет ли он к примеру сравнится просто с нативными вызовами?
Попробуем сравнить это, но так как в обычной жизни нам это может не пригодится, то добавим еще одно условие - сравниваем как лучший способ взаимодействия с jni библиотекой.
Постановка задачи
С давних времен рефлексия в jvm считается долгим и затратным процессом, каждый уважаемый себя разработчик на любом ревью мог попинать джуна - что за кошмарный код он использует через рефлексию. Сейчас времена другие, теперь джун стал умнее, но необходимость от использования рефлексии никуда не отпала.
К примеру, если вы пишете ndk приложения, то почти со 100% вероятностью вам понадобится использовать этот подход, чтобы просто смаппить данные из jvm в классические C++ классы или структуры. И да, этот маппинг не для слабонервных.
JNIEXPORT jint JNICALL
Java_com_example_engine_JniEngine_cmd(JNIEnv *env, jobject, jobject jCmd) {
struct some_cmd cmd{};
jfieldID idField = env->GetFieldID(env->FindClass("com/example/model/SomeCmd"), "id", "I");
jfieldID nameField = env->GetFieldID(env->FindClass("com/example/model/SomeCmd"), "name", "Ljava/lang/String;");
cmd.id = env->GetIntField(jCmd, idField);
jstring jName = (jstring) env->GetObjectField(jCmd, nameField);
const char *name = jName != NULL ? env->GetStringUTFChars(jName, NULL) : NULL;
cmd.name = std::string(name ?: "");
if (name != NULL) env->ReleaseStringUTFChars(jName, name);
}
Примерно так нужно маппить данные из jvm в нативной библиотеке ndk. И как сразу много проблем случается, если к примеру кто-то решит перенести модельку SomeCmd
из одного пакета в другой. Ну или переименовать поле или метод.
Каждый такой маппинг может использовать рефлексию один или несколько раз.
jfieldID idField = env->GetFieldID(env->FindClass("com/example/model/SomeCmd"), "id", "I");
jfieldID nameField = env->GetFieldID(env->FindClass("com/example/model/SomeCmd"), "name", "Ljava/lang/String;");
Это достаточно затратный процесс, в особенности, если полей и методов, которые мы ищем, много. Кажется простая сериализация моделей в какой-нибудь бинарный формат ускорит этот процесс, да и еще избавит нас от необходимости чистить ресурсы.
Просто Proto
Выбрав путь воина, тестировать планируем все на своем компьютере. А так как монструозных плагинов, поддерживающих и cmake и java, немного, автор может упомянуть только AGP, и он нам не подходит из-за специфичности платформы, то выбираем свой путь. Выполняем сборку проекта из нескольких плагинов для cmake, java, а также protobuf.
plugins {
application
id("com.github.gmazzo.buildconfig") version ("3.1.0")
id("io.github.tomtzook.gradle-cmake") version ("1.2.2")
id("com.google.protobuf") version ("0.9.3")
}
Используем классическую реализацию protobuf без grpc и kroto плагинов. Сам тест при этом ничего делает, только пересылает простую модельку в нативную библиотеку и обратно. Модельку сделали вложенной и из различных типов данных, чтобы приблизиться к реальному использованию.
syntax = "proto3";
option java_package = "com.github.klee0kai.proto";
message SomeCmdModel {
int32 id = 1;
int64 count = 2;
float value = 3;
double valueD = 4;
string name = 5;
repeated MetaModel meta = 6;
}
Для cmake проекта дополнительно нужно установить библиотеки protoc, к примеру для ubuntu.
apt-get install libprotobuf-dev protobuf-compiler
Генерируем модели C++ из proto файлов и подтягиваем библиотеку в CMakeLists.txt.
find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ./../proto/jni.proto)
target_link_libraries(myapplication ${Protobuf_LIBRARIES})
target_include_directories(
myapplication
PUBLIC
${JNI_INCLUDE_DIRS}
${PROTO_HDRS}
./../../../build/generated/sources/headers/java/main
)
Сами хедер файлы генерируются protobuf плагином в gradle, а работа с protobuf в C++ будет уже работать за счет, найденной в cmake, библиотеки. Проверяйте версии используемых библиотек и там и там, так как найденная в cmake библиотека protobuf может не переварить сгенерированные хедер файлы либо не поддержать различные опции, как например lite.
Так как в нашем проекте использованы несколько самостоятельных плагинов, никак не связанных между собой, немного корректируем последовательность выполнения задач в gradle. Сборку cmake будем выполнять после java - это нужно, чтобы сгенерировались jni хедер файлы для C++, а также сгенерировались хедер файлы proto моделек, которые в нашем случае генерируются через gradle.
tasks.clean.dependsOn(tasks.cmakeClean)
tasks.classes.dependsOn(tasks.cmakeBuild)
tasks.cmakeBuild.dependsOn(tasks.compileJava)
Сам проект, кстати, написан на java. Jni интеграция для kotlin выглядит сложнее. Так, например, многие kotlin типы в jni не доступны по своим именам классов:kotlin.Int
представляется либо через примитивный тип int
, либо через его объектное представление java.lang.Integer
. Kotlin не генерирует хедер файлы для jni, что в java поддерживается по умолчанию. Вдобавок, kotlin может исказить итоговые имена полей и методов.
Собрав все плагины воедино, получаем следующую последовательность сборки проекта.
:term:run
\--- :term:classes
+--- :term:cmakeBuild
| +--- :term:compileJava
| | +--- :term:generateBuildConfig
| | \--- :term:generateProto
| | +--- :term:extractIncludeProto
| | \--- :term:extractProto
| \--- :term:myapplication_linux-amd64_runGeneratorUnix_Makefiles
| \--- :term:cmakemyapplication_linux-amd64
+--- :term:compileJava
| +--- ***
\--- :term:processResources
\--- :term:extractProto
\--- ***
Осталось собрать и прогнать тест.
./gradlew run
Сухой остаток
Прогнав тест получаем результат
> Task :term:run
test jni reflection test...........
Test time 1.272
test on indexed jni reflection...........
Test time 0.594
test proto serialize...........
Test time 1.034
Protobuf получился быстрее рефлексии, однако не является лучшим решением для работы с jni. Самым оптимальным получилось решение использовать рефлексию с предварительным индексированием классов и методов - что заняло по итогу около 0.594 с. работы для 100к операции копирования и пересылки моделек в jni библиотеку и обратно.
Так как автор не провел замеры скорости работы нативной работы java, то предположим, что проиндексированная рефлексия является наиболее ее приближенным вариантом. Потери при использовании protobuf будут x2 от нативной работы, а при рефлексии - около x2.2. Подробнее о работе смотри на github.