Хочу с вами поделиться опытом переписывания с java на C++ на платформе Android и тем, что в результате получилось.

Для своего маленького домашнего проекта был использован алгоритм поиска лиц Виола-Джонса, java-исходники с моделью были взяты отсюда code.google.com/p/jviolajones с небольшой модификацией — были добавлены два класса: Point и Rectangle. Уточню почему не стал использовать OpenCV под Android — для его работы необходимо поставить отдельно приложение-библиотеку, что в моем случае будет весьма неудобно, и опыты показали его падение без предупреждения, разбираться с этим долго не стал, также и с поиском других библиотек, и решил взять простейшую готовую реализацию.

Быстродействие алгоритма показало плачевные результаты, на фотографии размером 400 на 300 на моем стареньком разбитом GT-I9300I — 54 секунды, на avd (виртуальном устройстве) и то дольше — 250 секунд.

Частенько попадались под мой взор обсуждения быстродействия кода на java и C++, где-то показывалось, что java отстает, в каких-то случаях даже и наоборот, приводились небольшие участки кода с одним циклом. Здесь же алгоритм чуть более, чем сложнее, порядка 6 вложенных циклов, как вы можете убедиться по исходникам. Поэтому было принято решение — испробовать на собственном опыте переписывание на C++. По всем прочитанным статьям у меня сложилось впечатление, что скорость повысится максимум процентов на 20, но как оказалось это было неверно.

Естественно встали следующие задачи — как передать входные и получить выходные данные и как переписать код. Заполнение модели из xml в конструкторе Detector решил оставить на java, заполняется, конечно, не быстро, но ввиду того, что работа с xml на C++ для меня звучит очень страшно, то оставил как есть. Род моей профессиональной деятельности связан с java, с C\C++ связывался только в институте и немного на работе по старым проектам. Поэтому пришлось изучить немного документации, почитать статьи и набить немного шишек.

Переписывание логики. Здесь особых проблем не возникло, был принят способ — не глядя скопировать классы, там, где eclipse подсвечивал красненьким проходился топориком. Все ArrayList'ы переделал в массив, благо — размер они не меняли.

Не буду описывать настройку среды для вызова нативного кода, статей на эту тему есть куча.

Как передать данные. С простыми типами — int, float, boolean все просто и понятно. С одномерным вроде бы тоже просто:

JNIEXPORT jint JNICALL Java_com_example_Computations_intFromJni(JNIEnv* env, jobject thiz, jintArray arr) {
	jsize d = env->GetArrayLength(arr);
	jboolean j;
	int * p = env->GetIntArrayElements(arr, &j);
...
}

С двумерным чуть посложнее:

JNIEXPORT jint JNICALL Java_com_example_Computations_findFaces(JNIEnv* env, jobject thiz, jobjectArray image) {
	int width = env -> GetArrayLength(image);
	jboolean j2;
	jintArray dim=  (jintArray)env->GetObjectArrayElement(image, 0);
	int height = env -> GetArrayLength(dim);
	int **imageLocal;
	imageLocal = new int*[width];
	for (int i = 0; i < width; i++) {
		jintArray oneDim= (jintArray)env->GetObjectArrayElement(image, i);
		int *element = env->GetIntArrayElements(oneDim, &j2);
		imageLocal[i] = new int[height];
		for(int j=0; j < height; ++j) {
			imageLocal[i][j]= element[j];
		}
	}
...
}

Поехали дальше, как передать объекты, у которых куча полей, среди которых есть типы List. Для получение поля объекта применяется следующая конструкция:

jclass clsDetector = env->GetObjectClass(objDetector);
jfieldID sizeFieldId = env->GetFieldID(clsDetector, "size", "Ldetection/Point;");
jobject pointObj = env->GetObjectField(objDetector, sizeFieldId);

Для листов нам понадобятся два метода get и size:

	jfieldID stagesFieldId = env->GetFieldID(clsDetector, "stages", "Ljava/util/List;");
	jobject stagesList = env->GetObjectField(detectorJObj, stagesFieldId);

	jclass listClass = env->FindClass( "java/util/List" );
	jmethodID getMethodIDList = env->GetMethodID( listClass, "get", "(I)Ljava/lang/Object;" );
	jmethodID sizeMethodIDList = env->GetMethodID( listClass, "size", "()I" );
	int listStagesCount = (int)env->CallIntMethod( stagesList, sizeMethodIDList );
	for( int i=0; i < listStagesCount; ++i )
	{
		jobject stage = env->CallObjectMethod( stagesList, getMethodIDList, i);
...

Данные научились получать. Запускаем, валится на ошибке — Local reference table overflow 512 entries. Получается, что необходимо чистить все локальные ссылки jclass и jobject, это делается так:

	env->DeleteLocalRef(jcls);
	env->DeleteLocalRef(jobj);

И для массивов тоже:

	env->ReleaseIntArrayElements(oneDim, element, JNI_ABORT);
	env->DeleteLocalRef(oneDim);

Возвращаем результат. Для упрощения своей задачи возвращение результата сделал в виде массива Rectangle:

	jclass cls = env->FindClass("detection/Rectangle");
	jobjectArray jobAr =env->NewObjectArray(faces->currIndex, cls, NULL);
	jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIII)V");
	for (int i = 0; i < faces->currIndex; i++) {
		Rectangle* re = faces->rects[i];
		jobject object = env->NewObject(cls, constructor, re->x, re->y, re->width, re->height);
		env->SetObjectArrayElement(jobAr, i, object);
	}
	return jobAr;

Итак, торжественный момент — поиск на той же фотографии — 14 секунд, т.е. в 4 раза быстрее, на других фотографиях аналогичные результаты. На виртуальном андроиде 132 секунды против 300 секунд. Но как нам известно, нельзя использовать результаты одного опыта, необходимо повторить несколько раз, приведу для одной фотографии, время обработка в секундах.
Виртуальное устройство Виртуальное устройство с использованием cpp Мой телефон galaxy Мой телефон galaxy с cpp
238 132 84 14
318 137 54 14
472 135 54 14
264 150 54 14
266 138 54 14
262 129 53 14

И в заключение отмечу. Несмотря на то, что переписывание дало большое ускорение, предела совершенству еще куча, можно использовать многопоточность, что я планирую изучить в ближайшее время. И внесение каких-либо корректировок в алгоритм — здесь наверное самая сложная часть.

update Выложил исходники. Использовать следующим образом:
	Detector detector = Detector.create(inputHaas);
	List<Rectangle> res = detector.getFaces(background_image, 1.2f, 1.1f, .05f, 2, true, useCpp);

inputHaas — поток модели, т.е. файл haarcascade_frontalface_default.xml из исходного алгоритма, useCpp — использовать C++ или нет. В исходниках C++ не делаю освобождение памяти, т.к. писал на скорую руку.

Комментарии (15)


  1. VasiliyKudryavtsev
    12.10.2015 15:16

    Интересно, конечно, но необходимость переписывания на C++ по статье оценить сложно.

    Вы профилировали Java-версию? Не было видно узких мест?
    На эмуляторе с Android 5 и средой Dalvik не запускали?

    И, если, можно покажите исходники C++ и Java версии


    1. QtRoS
      12.10.2015 19:05

      Средой ART имелось ввиду?


      1. VasiliyKudryavtsev
        12.10.2015 19:31

        Да, конечно


    1. trolleg
      12.10.2015 21:31

      Исходники приложил в конце статьи. Сначала хотел сделать профилирование, но «на глаз» не обнаружил узких мест, но, думаю, что займусь этим, т.к. самому очень интересно. Для эмуляции использовался Android 4.3.1, ART не использовал. Боевое устройство на Android 4.3.


  1. Dimezis
    12.10.2015 16:54

    Мне кажется, что это все равно очень плохой результат и стоит смотреть в первую очередь реализацию алгоритма, без привязки к языку.
    Что сразу бросилось в глаза — использование двумерного массива для работы с изображением. Это сильный удар по производительности и я считаю, что в таких алгоритмах следует использовать одномерную структуру.


    1. trolleg
      12.10.2015 21:33
      +1

      Имеется ввиду использовать вместо array[i][j] вот тако array[i * width + j]?


      1. DarthVictor
        13.10.2015 11:02

        Да, в Java array[i][j] — это именно массив массивов. Который будет создавать N+1 массив для матрицы размерности N. В Си это, насколько я знаю, это не так принципиально.


      1. Dimezis
        13.10.2015 13:53

        Именно так. В Android, например, все битмапы внутри содержат одномерный массив пикселей.
        Вы будете удивлены, на сколько быстрее обрабатывается одномерное представление.

        Вообще, метод Виолы-Джонса рассчитан на использование в реал тайме, так что вы должны понимать, что ваша реализация уж очень сильно проседает по производительности.


  1. Suvitruf
    12.10.2015 17:37
    +4

    Не первый раз уже вижу статьи о том как люди «переписывают Java алгоритмы на C++», при этом говоря, что алгоритм ускорился в 5, в 10, в 100 раз. Вот только я ни в одной этой статье не наблюдал, чтобы автор профилировал исходный Java код.

    «На Java алгоритм медленно работает, перепишу-ка я его на C++» — не правильный подход. Как по мне, разумнее найти проблемы именно в Java части, быть может их довольно просто пофиксить.


    1. trolleg
      12.10.2015 21:37

      Обязательно отпрофилирую, тем более уже вижу к чему как минимум стремиться.


    1. dyadyaSerezha
      13.10.2015 14:53

      Да, наверное. С другой стороны, если есть внутренняя неэффективность самого алгоритма, то она перейдет и в С++ версию. Хотя, например, вынести все локальные reference-переменные методов выше в класс надо было, конечно (исходники не смотрел, но это обычно очень большой замедлитель сложных численных алгоритмов на Джаве).


    1. 0leGG
      14.10.2015 03:40

      Я вот прямо чувствую, что переписав парсер XML (на использование того же SAX), уже можно догнать и перегнать C++. Не говоря уж о том, что и как можно сделать, если оптимизировать java-код.


      1. trolleg
        14.10.2015 17:38

        Да, парсер здесь не быстрый, омобенно это заметно на виртуальном устройстве. Парсер в замере скорости не участвовал, предполагается, что он один раз инициализируется.


  1. 0leGG
    14.10.2015 03:41

    А не могли бы вы выложить проект полностью, чтобы можно было его рррраз, и собрать, а не играть в настраивание билда? Хочется на досуге попытаться его оптимизировать без влезания в нативный код.


    1. trolleg
      14.10.2015 17:44

      Весь проект выложить не могу, там секретные коды:)