В целях улучшения производительности приложения на Андроид начал постепенно переписывать критические участки кода с Java (SDK) на С++ (NDK). Результат оказался сравнимым с тем, что я получил пару десятков лет назад, делая ассемблерные вставки в код турбопаскаля.
Я не ставлю перед собой задачи описать работу с Android NDK — у самого недостаточно опыта. Тем, кто заинтересуется, лучше начать с этой ссылки.
Цель данной короткой статьи — привести несколько цифр, которые я получил опытным путем, сравнивая время выполнения определенных функций, написанных на Java и после этого переписанных на C++. И, возможно, эти цифры мотивируют кого-либо поглубже изучить этот вопрос.
Так как мое приложение связано с обработкой фотографий, то узкими местами являлись циклы обхода пикселей картинки и определенных действий над ними. Тестировал я на реальных устройствах — Nexus One и Nexus 7 (2012). Результаты экспериментов (в ms) свел в таблицы:
В среднем выигрыш в скорости для Nexus One — в 21 раз, для Nexus 7 — в 36 раз.
В среднем выигрыш в скорости для Nexus One — в 112 раз, для Nexus 7 — в 82 раза.
В среднем выигрыш в скорости для Nexus One — в 4 раза, для Nexus 7 — в 5 раз.
Как видим, результаты различаются на один, а то и два порядка. Я специально привел цифры в абсолютных значениях, чтобы было видно реальное ускорение работы от применения NDK. Сравнительно скромные результаты последнего теста обусловлены тем, что для расчета наложения использовались в том числе и стандартные функции библиотеки OpenCV, которые достаточно хорошо оптимизированы. Соответственно данный тест наглядно показывает реальное ускорение работы приложения в целом.
Вскользь коснусь применения библиотеки OpenCV. Как я и ожидал, Java-часть библиотеки является обычной оберткой над NDK. Все же провел вышеописанные эксперименты над достаточно тяжелыми и долгоиграющими алгоритмами — такими как нахождение характерных точек на изображениях, grabcut — метод. Разница в скорости между Java и NDK составила максимум 10%, что можно списать на погрешность, так как совершенно одинаковых изображений в тот момент я получить не мог.
Update. Довольно неприятно признавать собственные ошибки, но что делать.
Итак, вот пример кода, с помощью которого я оценивал производительность Java-реализации библиотеки OpenCV:
Обходим попиксельно две матрицы одинакового размера и в зависимости от значения соответствующего пикселя той и другой матрицы рассчитываем результирующий пиксель.
Благодаря замечаниям в комментариях к статье код был оптимизирован следующим образом (рисунки одноцветные):
Для тестирования опять же использовал реальные устройства Nexus One и Nexus 7, на вход же подавал 3-х мегапиксельные картинки и в том и другом случае — хотел попутно сравнить производительность устройств между собой. Результаты (средние, в ms) свел в таблицу:
Выводы каждый может сделать сам. Оптимизация кода в С++ проводилась по тому же принципу, что и в Java. Код не привожу, он однотипен вышеприведенному.
Я не ставлю перед собой задачи описать работу с 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. Код не привожу, он однотипен вышеприведенному.