Пользователь
0,0
рейтинг
13 марта 2014 в 18:55

Разработка → Android SDK vs NDK — сравнение производительности однотипных участков кода из песочницы

В целях улучшения производительности приложения на Андроид начал постепенно переписывать критические участки кода с Java (SDK) на С++ (NDK). Результат оказался сравнимым с тем, что я получил пару десятков лет назад, делая ассемблерные вставки в код турбопаскаля.

Я не ставлю перед собой задачи описать работу с Android NDK — у самого недостаточно опыта. Тем, кто заинтересуется, лучше начать с этой ссылки.
Цель данной короткой статьи — привести несколько цифр, которые я получил опытным путем, сравнивая время выполнения определенных функций, написанных на Java и после этого переписанных на C++. И, возможно, эти цифры мотивируют кого-либо поглубже изучить этот вопрос.

Так как мое приложение связано с обработкой фотографий, то узкими местами являлись циклы обхода пикселей картинки и определенных действий над ними. Тестировал я на реальных устройствах — Nexus One и Nexus 7 (2012). Результаты экспериментов (в ms) свел в таблицы:

Наложение слоя (режим Luminosity, цветной рисунок)

Nexus One Nexus 7
SDK NDK SDK NDK
2563 120 4850 90
2122 100 4520 190
2162 110 4330 100

В среднем выигрыш в скорости для Nexus One — в 21 раз, для Nexus 7 — в 36 раз.

Наложение слоя (режим Color Dodge, одноцветный рисунок)

Nexus One Nexus 7
SDK NDK SDK NDK
2673 30 5720 80
2572 20 6230 70
2573 20 6110 70

В среднем выигрыш в скорости для Nexus One — в 112 раз, для Nexus 7 — в 82 раза.

Наложение слоев по градиенту прозрачности

Nexus One Nexus 7
SDK NDK SDK NDK
1301 321 3010 470
1221 330 2670 620
1211 300 2770 610

В среднем выигрыш в скорости для Nexus One — в 4 раза, для Nexus 7 — в 5 раз.

Как видим, результаты различаются на один, а то и два порядка. Я специально привел цифры в абсолютных значениях, чтобы было видно реальное ускорение работы от применения NDK. Сравнительно скромные результаты последнего теста обусловлены тем, что для расчета наложения использовались в том числе и стандартные функции библиотеки OpenCV, которые достаточно хорошо оптимизированы. Соответственно данный тест наглядно показывает реальное ускорение работы приложения в целом.

Вскользь коснусь применения библиотеки OpenCV. Как я и ожидал, Java-часть библиотеки является обычной оберткой над NDK. Все же провел вышеописанные эксперименты над достаточно тяжелыми и долгоиграющими алгоритмами — такими как нахождение характерных точек на изображениях, grabcut — метод. Разница в скорости между Java и NDK составила максимум 10%, что можно списать на погрешность, так как совершенно одинаковых изображений в тот момент я получить не мог.

Update. Довольно неприятно признавать собственные ошибки, но что делать.
Итак, вот пример кода, с помощью которого я оценивал производительность Java-реализации библиотеки OpenCV:
for (int i=0; i<mat.rows(); i++){
	for (int j=0; j<mat.cols(); j++) {
		double[] matPix = mat.get(i, j);
		double[] topPix = top.get(i, j);
		if (matPix[0]+topPix[0]>255){
			matPix[0] = 255.;
		} else {
			matPix[0] = (255. * matPix[0]) / (256. - topPix[0]);
		}
		mat.put(i, j, matPix);
	}	
}

Обходим попиксельно две матрицы одинакового размера и в зависимости от значения соответствующего пикселя той и другой матрицы рассчитываем результирующий пиксель.
Благодаря замечаниям в комментариях к статье код был оптимизирован следующим образом (рисунки одноцветные):
int size = mat.cols();
byte[] matPix = new byte[size];
byte[] topPix = new byte[size];
for (int i=0; i<mat.rows(); i++){
    mat.get(i, 0, matPix);
    top.get(i, 0, topPix);
    for (int j=0; j<size; j++) {
    	int mp = (matPix[j] & 0xFF);
    	int tp = (topPix[j] & 0xFF);
        if (mp+tp>255){
            mp = 255;
        } else {
            mp = (255 * mp) / (256 - tp);
        }
        matPix[j] = (byte) mp;
    }
    mat.put(i, 0, matPix);
}

Для тестирования опять же использовал реальные устройства Nexus One и Nexus 7, на вход же подавал 3-х мегапиксельные картинки и в том и другом случае — хотел попутно сравнить производительность устройств между собой. Результаты (средние, в ms) свел в таблицу:

Nexus One Nexus 7
SDK NDK SDK NDK
Без оптимизации 35404 245 22755 160
С оптимизацией 340 205 210 120

Выводы каждый может сделать сам. Оптимизация кода в С++ проводилась по тому же принципу, что и в Java. Код не привожу, он однотипен вышеприведенному.
Игорь Бикинеев @websiteam
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +1
    Который раз уже вижу подобные сравнения (:

    Вообще, про это в офф. документации написано хорошо:
    Typical good candidates for the NDK are self-contained, CPU-intensive operations that don't allocate much memory, such as signal processing, physics simulation, and so on.

    Так что, не во всех задачах C++ даст такой большой прирост производительности.

    Но в статье меня кое-что другое интересует — каким образом это тестировалось? Какие алгоритмы? На каких контейнерах? И т.д.
    Потому что, если говорить про Java, быть может у вас большую часть времени работы занимали не сами алгоритмы, а работа с контейнерами как таковая.
    • +1
      Именно сам алгоритм. Вот как то так:
      public static boolean ColorDodgeGray(Mat mat, Mat top){
      		
      		Log.i(TAG,"ColorDodgeGray BEGIN");
      		
      		NativeUtils.nativeColorDodgeGray(mat.getNativeObjAddr(), top.getNativeObjAddr());
      		
      		/*for (int i=0; i<mat.rows(); i++){
      			for (int j=0; j<mat.cols(); j++) {
      				double[] matPix = mat.get(i, j);
      				double[] topPix = top.get(i, j);
      				if (matPix[0]+topPix[0]>255){
      					matPix[0] = 255.;
      				} else {
      					matPix[0] = (255. * matPix[0]) / (256. - topPix[0]);
      				}
      				mat.put(i, j, matPix);
      			}	
      		}*/
      		Log.i(TAG,"ColorDodgeGray END");
      		return true;
      	}
      
      • +5
        Я так понял что аналог нейтивному коду — это закоментированный код?
        Если так, тогда это просто неправильный подход. В Android есть быстрые средства для работы с изображениями. Смотрите например ColorMatrix и Paint.setXfermode.
        Если же нужно обработать именно Mat, тогда очевидно лучшим решением будет использовать предоставляемые методы, или сконвертировать его в Bitmap и использовать упомянутые методы.
        • +3
          Ну и раз речь идет о попиксельной обработке то нужно упомянуть о open gl шейдерах. Этот подход «порвет как тузик тряпку» NDK код.
          • +2
            Эм, а что мешает использовать OpenGL и шейдеры в ndk?
            • +3
              Мешает? Ну в данном примере мне бы помешала лень. Зачем возиться с NDK если то-же самое можно сделать в Java.
        • 0
          Я так понял что аналог нейтивному коду — это закоментированный код?

          Совершенно верно.
          Если же нужно обработать именно Mat, тогда очевидно лучшим решением будет использовать предоставляемые методы, или сконвертировать его в Bitmap и использовать упомянутые методы

          А что Вы подразумеваете под предоставляемыми методами? Методы OpenCV? Так я их и использую в 90% случаев и только там, где нет подходящего метода, пишу свой. Был, кстати, еще случай, когда приложение вылетало при применении стандартной функции OpenCV — поэлементного умножения матриц. Вот тут тоже пришлось переписать ее на C++.
          Что касается преобразований Mat -> Bitmap и обратно, то это очень затратные функции и по времени и по памяти.
          • 0
            А что Вы подразумеваете под предоставляемыми методами? Методы OpenCV?

            Да.
            Если нужна все таки обработка исходного Mat, то вероятно NDK будет лучшим выбором.
            Но тестировать производительность приведенного метода — это абсурдная идея, так как демонстрирует неправильный подход.
      • 0
        Ну как же, вот у вас работа с коллекциями: в каждой итерации цикла 2 раза Mat.get и 1 Mat.put. Кто его знает, сколько они времени тянут. В С++ версии они тоже есть?
        • 0
          Там их заменяют
          uchar matPix = mat.at<uchar>(i, j);
          mat.at<uchar>(i, j) = matPix;
          • +2
            Но в нативном коде это шаблонный класс, а значит, наверняка инлайновый и прекрасно оптимизируется (и из его метода возвращается ровно один байт!), а вот
            double[] matPix = mat.get(i, j);
            
            судя по возвращаемому массиву по-любому делает копирование массива (rows*cols раз!), из которого потом используется ровно один элемент…
            В общем, Java-код действительно странноват.
            Вот, например, существенно более быстрый способ: ровно одно копирование.
            Ну и, rows() и cols() стоило бы вызвать один раз.
            • 0
              Вот, например, существенно более быстрый способ: ровно одно копирование.

              Верно. Только это удвоение (утроение в данном примере) используемой памяти. А если все операции происходят с 13 MP картинкой?
              • +1
                Даже так, можно делать то же самое построчно, один раз создав буфер. Уж на четыре килобайта памяти точно хватит.
                • 0
                  К тому же, этот буфер можно сделать типа byte[], что более эквивалентно NDK-коду.
                • 0
                  Я согласен. В некритичных случаях приведенный Вами пример вполне подходит.
                  • +2
                    Я всё-таки говорю о том, что даже «критичный» случай на Java может работать существенно быстрее с минимумом усилий.
                    В данном случае, в нативном коде для get() вместо аллокации массива через NewDoubleArray на каждом пикселе делалось бы GetPrimitiveArrayCritical (одна аллокация на весь массив).
                    Отсюда вопрос: во сколько раз это было бы быстрее?
                    • +2
                      В выходные постараюсь проверить. Самому интересно.
                      • +2
                        Буду ждать, мне тоже интересно. :)
                        Опять же, про double всё ещё не очень понятно: поскольку в «NDK-коде» используется uchar, то я полагаю, что матрица используется CV_8U? Если так, то даже сами по себе методы get и put делают поэлементное преобразование типов вместо выполнения обычного memcpy, что также не даёт преимуществ Java-коду. Проверьте заодно и это, если не затруднит.
                      • 0
                        Тоже отмечусь, чтобы посмотреть чем закончилось.
                      • +1
                        Попробую дать развернутый ответ.
                        Во-первых, вот этот код не работает.
                        В приведенном мною выше примере код

                        double[] matPix = mat.get(i, j);

                        работает для матрицы любого типа. Если же мы создадим массив типа double и попробуем туда скопировать данные матрицы типа CV_8U

                        double[] buff = new double[size];
                        mat.get(0, 0, buff);

                        то произойдет исключение. Залез в исходники OpenCV — все верно, проверяется, правильного ли типа матрица, если нет — возбуждается исключение. Попробовал объявить нужный тип буфера — как сказано здесь. Переписал вышеприведенный код на этот:

                        int size = mat.cols();
                        for (int i=0; i<mat.rows(); i++){
                        	byte[] matPix = new byte[size];
                        	byte[] topPix = new byte[size];
                        	mat.get(i, 0, matPix);
                        	top.get(i, 0, topPix);
                        	for (int j=0; j<size; j++) {
                        		if (matPix[j]+topPix[j]>255){
                        			matPix[j] = (byte) 255;
                        		} else {
                        			matPix[j] = (byte) ((255 * matPix[j]) / (256 - topPix[j]));
                        		}
                        	}
                        	mat.put(i, 0, matPix);
                        }

                        Все работает, но вместо эффекта Pencil sketch у меня получается кадр из фильма «Хищник». Почему так? — byte[] в Java — массив со знаком. В OpenCV get и put с массивами типа uchar (пока?) не работают. Но этот оптимизированный код работает быстро. У меня получилось 60, 50 и 50 миллисекунд против 30, 20 и 20 неоптимизированного NDK. Создание еще одного (двух в моем случае) буферных массивов типа char, преобразование данных, работа с ними, затем обратное преобразование возможно даст эффект, но, по-моему, использование NDK для этих целей проще.
                        • 0
                          Про то, что буфер должен быть byte, я уже два раза написал. :)
                          Попробуйте вот так:
                          int mp = (matPix[j] & 0xFF);
                          int tp = (topPix[j] & 0xFF);
                          

                          (ну и, понятно, дальше работать с этими целыми, а не с массивами, заодно проверка на ">255" будет корректной). Вроде, должно сработать правильно.

                          И, кстати, спасибо за тест. :)
                          • 0
                            А, и ещё забыл: надо было «new byte[size]» написать до цикла, чтобы аллоцировать память один раз, а не rows() раз. Может оказаться ещё немного быстрее.
                          • 0
                            Не очень понял смысл конъюнкции в данном случае.
                            Если matPix[j] выражение имеет тип byte (или char в С), и если конъюнкция использовалась, чтобы предостеречься от записи мусора, то и Java и C определяют так называемый integral promotion, который гарантированно избавит от побитовых нелепостей.
                            • +1
                              Её смысл в том, чтобы значение 0xFF, например, преобразовалось в 255, а не в -1.
                          • 0
                            И, кстати, спасибо за тест. :)

                            Вам спасибо за ценные советы. Дополнил статью новыми тестами.
          • 0
            Если хотите перформанса, е используйте конструкции вида mat.at(idx, idy), при многократном обращении таким образом(например в цикле по всей картинке) перформанс ощутимо проседает. Лучше ковырнуть сырой указатель примерно так: (uchar)mat.data и работать с ним… так получается быстрее для поэлемнтной обработки. Да и имеет смысл в таких местах задуматься об использовании NEON или чегото подобного.
  • +1
    Neon использовали?
    • +1
      Нет.
  • –8
    Природу не обманешь:) Процессор умеет лишь выполнять инструкции, и ему пофиг что это за инструкции — Вашей программы, JIT-компилятора или интерпретатора байт-кода. Хотя прошлый раз народ почему-то не согласился :)
    • +2
      Ну я вот до сих пор не понял, к чему был что тот ваш коммент, что этот…
  • 0
    Renderscript пробовали?
    • 0
      Нет. И, как я понимаю, он только с 3-го Андроида работает?
  • +1
    Я вот только не понял — почему на древнем nexus one работает все в пару раз быстрее, чем на Nexus 7, который уже, конечно, не самый новый девайс, но 4 ядра и гиг оперативки все же имеет
    • +1
      Сравнивать скорость у девайсов в данном случае некорректно, так как тестировались копии картинки, уменьшенные пропорционально размеру экранов устройств.
    • +1
      количество ядер не играет тут никакой роли, код выполняется в одном потоке
  • –5
    ШОК. Всегда считал, что джава быстрей ассемблера. но если серьозно, 36 раз это перебор. Джава на андроиде медленней, но не в 36-112 раз. Такое может быть если в джаве происходит частое выделение памяти, но то там есть оптимизации, поколения. Бейсик на БК0010 был 100 раз медленней ассемблера. джава может быть 100 медленней шейдера, но не нативного кода.
    • 0
      Спокойствие, автор просто не умеет готовить Java.
  • +1
    Ну молодцы, конечно… Из JAVA считать мульоны пикселей и жаловаться, что все плохо. Опускаем на уровень NDK и пишем какой код для трансформаций (да, я писал такой) – полуаем терпимый результат. Но не более. Пишем то же самое на шейдерах (да, прямо в java классе) – и радуемся отличной производительности, + никакой возни с NDK.
    • 0
      и на засыпку… Мне лень не нельзя было ставить OpenCV, и мне пришлось разбирался с цветовыми моделями. У вас точное совпадение по цветам получилось с фотошопом, используя openCV? Результаты тестирования алгоритмов различных реализаций дали разные результаты: тот же gimp и photoshop вели себя по раному. Фотошопный вариант у меня получился только с применением шейдеров.
  • 0
    Что за класс Mat?
    • 0
      контейнер OpenCV'шный

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.