Я пишу приложение, которое должно использовать поля jobject
несколько раз с большой скоростью. Поскольку получение полей из jobjects
происходит довольно медленно, я хочу сделать это один раз, а затем читать и записывать поля какого-нибудь объекта C++ или элементы многотипного массива (если таковой может быть создан) или что-то еще, что обеспечивает быстрый доступ к данным (далее - целевой контейнер ).
Мой код имеет две функции: первая записывает поля jobject
в целевой контейнер, вторая считывает из него записанные данные.
Целевой контейнер должен быть способен хранить переменные разных типов, и его размер не должен определяться во время компиляции (поэтому struct
не подходит). Вопрос в следующем: какой контейнер лучше всего удовлетворяет этим условиям, если его качество определяется скоростью чтения (+преобразования типов в правильные) из него?
Прямо сейчас я использую vector<any>
, как показано ниже:
#include <vector>
#include <any>
JNIEXPORT void JNICALL Java_runTask(JNIEnv*env,jclass,jobject o){
vector<any>d;
readData(d,/*some other arguments*/,o);
do{
//do something useful
}while(predicate);
}
void readData(vector<any>*data/*some other arguments*/,jobject o){
//fill the vector with jobject's fields' values
}
bool predicate(vector<any>*data){
//determine the return value using vector contents
return true;
}
vector<any>
соответствует моим условиям, НО...
any_cast<type>()
ПРОВЕРЯЕТ тип перед регистром, что означает, что он имеет несовершенную производительность.@Eljay, чтение целого числа из контейнера std::vector<std::any>
происходит примерно в 20 раз быстрее, чем получение целого числа через JNI. Это важно.
Под «Я не знаю его размера» вы имеете в виду, что не знаете, каков тип значения хеш-карты? Или вы не знаете количество элементов в хэш-карте?
@Wutz, я не знаю обоих. Я имею в виду, что я не знаю, что будет передано в функцию во время выполнения, поэтому я не могу подготовить контейнер только для одного типа или контейнер с размером, предопределенным во время компиляции.
@Wutz Я не знаю ни того, ни другого. Вы можете получить размер, используя FindClass("Ljava/util/HashMap;")
, чтобы получить класс Java, а затем GetMethodID
, чтобы получить идентификатор метода функции size
. Затем вызовите CallIntMethod
для идентификатора класса и метода.
@PaulMcKenzie, я знаю, что могу легко получить его размер, но некоторые подходы могут включать использование, например, структур с ограниченным количеством полей std::any
, которые не подходят, поскольку количество полей определяется во время компиляции, и я хочу избегайте подобных рекомендаций.
Я думаю, что любой вектор кажется лучшим решением, учитывая ваши текущие ограничения. Но мне интересно: если вы вообще не знаете тип значения, как вы вообще извлекаете информацию для хранения в any
? Если это может быть только ограниченное количество различных типов значений, вместо этого вы можете использовать std::variant<...>
.
Это своеобразный шаблон доступа. Как часто сторона Java читает и записывает свои данные? Как часто родная сторона читает/пишет?
@Botje, все делается в коде C++, но он сначала записывает несколько env->CallObjectMethod
«расшифрованных» (удобных для использования) результатов в какое-то хранилище (vector<any>
, в моем случае), а затем считывает все необходимое из этого хранилища.
Если все делается на собственной стороне, почему данные вообще хранятся на стороне Java? Рассмотрите возможность сериализации состояния Java в байтовый буфер для чтения C++ с использованием, например, плоских буферов или буферов протокола.
@Botje, я неправильно это объяснил. Под «всем» я подразумеваю все, начиная с первого вызова метода JNI. Точнее, программный цикл делится на два этапа: взаимодействие с пользователем через Java (и запись полей) и выполнение автоматического задания через C++. Поскольку первый этап пройден, все делается на нативной стороне, поэтому мы можем считать все задания постоянными и считанные из них данные всегда актуальны.
JAVA не очень любит C++. JNI — это минимальная совместимость с C. Любое правильное решение на C++ требует оболочки C API. Официального API C++ не существует, поэтому ответственность лежит на программисте. Вы можете просмотреть различные проекты с открытым исходным кодом и выбрать подходящий. В противном случае вы можете отметить сохранение родной части.
Тогда я повторяю: передайте плоский буфер нативной стороне.
@Ботье, почему? Мне удалось прочитать данные из заданий, мне нужно найти наиболее подходящее хранилище для многотипных данных.
@Red.Wave, Ботье, ты неправильно истолковал мой вопрос. Цель — найти многотипное хранилище с максимальной скоростью чтения.
HashMap<String,Object>
— красивый тип сообщения для отправки через языковую границу. Можете ли вы сделать лучше, например? HashMap<String, SomeDTO>
? И если вы можете это сделать, могли бы вы вместо этого сериализовать эти SomeDTO
и вообще не использовать JNI?
@Калет, пожалуйста, прочитай обновленный вопрос (в конце). Обратите внимание: у меня нет проблем с использованием jobject
, мне просто нужен объект/массив, куда я могу поместить все извлеченное из него. У меня даже есть такой, но должен быть вариант получше.
Можете ли вы хотя бы рассказать нам, какие типы вы извлекаете из Java? Возможно, все это укладывается в общую структуру данных, подобную той, что используется библиотеками JSON. И тогда вы могли бы вместо этого передать JSON-объект через границу JNI.
@Botje, этот объект не будет передаваться с одного языка на другой, я буду использовать контейнер только в C++. Что касается типов, то, к сожалению, я не могу предсказать, какие типы буду использовать в будущем, поэтому подойдет любой тип.
«Лучшая скорость чтения» — не использовать JNI.
«Я не могу предсказать, какие типы я буду использовать в будущем». Почему это должно подходить каждому типу? будет быстрее, если он специализируется на одной задаче
Я сдаюсь. Вы можете иметь максимальную гибкость или максимальную скорость, но не то и другое. Прими решение.
@Калет, позволь мне еще раз прояснить ситуацию: у меня есть jobject
, информация о котором считывается один раз (это место не требует скорости). Эта информация должна храниться в каком-то контейнере (далее – целевой контейнер). Данные из целевого контейнера будут считываться несколько раз, и этот процесс должен быть максимально быстрым. Цель (и помощь, о которой я спрашиваю) состоит в том, чтобы определить, какой контейнер использовать в качестве целевого контейнера. Обратите внимание: при чтении из него не будут использоваться JNI, Java и все, что связано с языками высокого уровня.
Самым быстрым будет SomeSpecificType
, заточенный ровно под одну задачу. Может быть, сделать шаблоны readData
и predicate
? Возможно, если бы все поля были одного типа, вы могли бы иметь std::vector<SomeOtherType>
@Калет, я думаю, декодирование всего из одного типа потребует больше времени, чем преобразование any
в необходимый тип. Шаблоны исключают возможность многотипного хранения. Один из выходов, который я вижу, — это использовать что-то вроде any
, но без проверки безопасности типов при приведении типов (если такая вещь вообще существует).
Можете ли вы определить конкретный интерфейс того, что вы хотите делать с этими произвольными данными? class BaseTask { virtual foo frobnicate() = 0; /* etc */ };
и template<typename T> class Task<T> { foo frobnicate() override { /* do T specific stuff */ } };
Предположим, я передаю вам вектор any
, не сообщая, какие реальные типы any
могут храниться. Что ты можешь сделать с этим?
Как говорилось ранее, основная проблема — плохой API JNI. Он содержит много повторяющегося строкового кода, написанного очень примитивным образом. Одной из основных задач будет преодоление недействительности ссылок, происходящих при вызовах модуля C++. Для этого вам действительно нужно использовать стороннюю оболочку C++; необходимо изучить множество основ C++, прежде чем вы сможете справиться с этим самостоятельно.
@n.m.couldbeanAI, у меня есть две функции, обе состоят из выражения switch
, которое получает число на основе данных, которые должны храниться в целевом контейнере. Они «знают», что делают. Все, что мне нужно, это просто передать этот контейнер из одной функции в другую. Вторая функция будет выполняться много раз, поэтому мне нужен многотипный контейнер.
@Калет, пожалуйста, прочитай мой комментарий выше. Я не понимаю, зачем мне такой интерфейс.
Если кто-то знает, как улучшить структуру моего вопроса, чтобы было понятно тому, кто придет после нас, подскажите, пожалуйста. Как я вижу, сейчас это совершенно непонятно.
Нет, мы понимаем ваш вопрос. Мы просто думаем, что у вас проблема XY
Не уверен, что именно вы пытаетесь сделать, но первая реакция заключается в том, что каждая ветвь вашего переключателя должна быть отдельной функцией, и контейнер должен хранить эту функцию вместе с данными для работы, то есть привязкой std::function
.
Любое решение, которое позволяет хранить произвольные типы в контейнере, а код, взаимодействующий с этим контейнером, волшебным образом зная, к чему приводить, будет медленнее и содержит больше ошибок, чем код, в котором с каждым фрагментом данных связан статический тип. Интерфейс, который я написал выше, предлагал вам поднять полиморфизм с data
, чтобы также включить операции, которые вы выполняете с этими данными.
@Caleth, я новичок в C++, и с этого момента я ничего не понимаю :( Я изо всех сил старался переописать проблему в вопросе, чтобы сосредоточиться на очень конкретной задаче. Буду рад, если вы попробуйте объяснить подход с полиморфизмом еще раз (но это не обязательно).
Вместо того, чтобы пытаться использовать контейнер, который может хранить произвольные данные, мы можем обернуть прочитанные и используемые данные в полиморфный тип.
В качестве эскиза вам придется переработать Java_runTask
, чтобы не иметь дело с данными напрямую.
struct BaseTask
{
virtual bool predicate() = 0;
virtual Result process() = 0; // whatever "do something useful" does
};
std::unique_ptr<BaseTask> readData(JNIEnv* env, jclass c, jobject o)
{
// based on c, construct a subclass of BaseTask, and read the data from o into it
}
JNIEXPORT void JNICALL Java_runTask(JNIEnv*env,jclass,jobject o){
auto task = readData(/*some other arguments*/,o);
do{
auto res = task->process();
// do more processing
} while(task->predicate());
}
Затем вам потребуются подклассы BaseTask, специфичные для разных типов данных. Один из способов сделать это — создать подкласс шаблона.
template <typename T>
struct Task : public BaseTask
{
T data;
bool predicate() override;
Result process() override;
};
template <>
bool Task<Foo>::predicate()
{
// data is a Foo here
}
template <>
bool Task<Bar>::predicate()
{
// data is a Bar here
}
По сути, это то же самое, что определение
class FooTask : public BaseTask { /* all the members */ };
class BarTask : public BaseTask { /* all the members */ };
О Конечно! Предикат может быть построен динамически! Это похоже на структуру, которая у меня есть в Java (где я создаю новый Consumer<>
для каждой команды) (которая оказалась слишком медленной из-за безопасности языка). Большое спасибо.
@Eljay, если честно, я не замерял производительность, просто где-то прочитал, что "общаться" между C++ и Java (в обоих направлениях) обходится дороже. Я собираюсь провести измерение прямо сейчас. Я обновлю вопрос и сообщу вам, как только буду готов.