Разрабатывая игровой движок для Android, я был уверен, что нативный код C/C++ будет исполняться быстрее чем аналогичный код на Java. Это утверждение справедливо, но не для последних версий Android. Чтобы проверить почему так происходит, решил провести небольшое исследование.
Для теста использовался Android Studio 4.1.3 - для Java Android SDK (API 30), для C/C++ Android NDK (r21, компилятор CLang). Тест довольно тупой, который выполняет арифметические операции над массивом int в двух вложенных циклах. Ничего осмысленного и специфичного.
Вот метод, написанный на Java:
public void calculateJava(int size) {
int[] array = new int[size];
int sum = 0;
for (int i=0; i<size; i++) {
array[i] = i;
for (int j=0; j<size; j++) {
sum += array[i] * array[j];
sum -= sum / 3;
}
}
}
Вот метод на C/C++ (осознанно не освобождаю память для чистоты сравнения с Java GC):
extern "C" JNIEXPORT void JNICALL Java_com_axiom_firstnative_MainActivity_calculateNative(
JNIEnv* env,
jobject,
jint size) {
int* array = new int[size];
int sum = 0;
for (int i=0; i<size; i++) {
array[i] = i;
for (int j=0; j<size; j++) {
sum += array[i] * array[j];
sum -= sum / 3;
}
}
// delete[] array;
}
Вот метод, который вызывает тесты Java:
long startTime = System.nanoTime();
calculateNative(4096);
long nativeTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
calculateJava(4096);
long javaTime = System.nanoTime() - startTime;
String report = "VM:" + System.getProperty("java.vm.version")
+ "\n\nC/C++: " + nativeTime
+ "ns\nJava: " + javaTime + "ns\n"
+ "\nJava to C/C++ ratio "
+ ((double) javaTime / (double) nativeTime);
Вот такие результаты получились на разных устройствах
Samsung Galaxy Tab E (Android 4.4.4) :
Java time: 2 166 748 ns
C/C++ time: 396 729 ns (C/C++ быстрее в 5 раз )
Prestigio K3 Muze (Android 8.1):
Java time: 3 477 001ns (первый запуск)
C/C++ time: 547 692ns (C/C++ в 6 раз быстрее),
НО при повторном запуске теста Java выполняется лишь на 30-40% медленнее (разогрев?).
Samsung Galaxy S21 Ultra (Android 11):
Java time: 111 000ns
C/C++ time: 121 269ns
Интересно: Java на 9% медленнее при первом запуске и 40-50% быстрее C/C++ при втором.
Включение флагов оптимизации компилятора CLang (-O3) делают код C/C++ быстрее на ~30-35% (Prestigio K3 Muze Android 8.1) чем Java код, даже при втором запуске.
Но на Smasung Galaxy S21 Ultra (Android 11) Java код выполняется на 10-20% быстрее чем нативный С/C++ код скомпилированный CLang с включенными флагами оптимизации (-O3). Это чуток взорвало мой мозг...
p.s. Оба теста запускаются последовательно в одном потоке одного процесса, поэтому предполагаю, что они используют одно и то же ядро CPU.
И тут возник вопрос, почему Java код на последних Android устройствах выполняется быстрее аналогичного C/C++ кода? Как это вообще возможно?
А возможно это следующим способом. В Android Runtime наряду с Ahead-of-Time компиляцией Java кода в нативный код, по прежнему работает механизм Just-In-Time компиляции на основе собранной статистики первого запуска приложения. Где имея данные о часто выполняемых участках кода можно провести более эффективные оптимизации. Вот так примерно говорят в документации:
The JIT compiler complements ART's current ahead-of-time (AOT) compiler and improves runtime performance. Although JIT and AOT use the same compiler with a similar set of optimizations, the generated code might not be identical. JIT makes use of runtime type information can do better inlining and makes on stack replacement (OSR) compilation possible, all of which generate slightly different code.
Есть ли смысл переписывать приложения на Java на NDK C/C++ для производительности?
Как мне кажется, для старых устройств с виртуальной машиной Dalvik VM (до 7.0 версии Android) - однозначно, да. Что касается более новых устройств, с Android версии выше 7.0 (где используется среда выполнения ART), это не имеет большого смысла, если только вы не опытный С/C++ разработчик, глубоко понимающий как работает CPU и способный сделать оптимизации лучше Android Runtime. Да и игра не стоит свеч (эффект/усилия), за исключением следующих случаев:
Вы портируете имеющееся приложение на C/C++ на Android
Вы хотите использовать С/C++ библиотеки не доступные Java
Вы хотите использовать API не доступные в Android SDK
P.S. Если у вас есть какие-то соображения, буду рад комментариям.
samoreklam
Возможно java работает быстрее, т.к. делает оптимизацию для попадания в кеш процессора.
Те. преобразовывает цикл
В нормальный вид. Когда умножаемое и множитель, расположены рядом друг с другом в памяти (а точнее в L1 кеше процессора).
Цикл разбивается на два независимых цикла.
Т.е. сначала заполняется полностью массив array (т.к. после присвоения, над array[i] нет действий с присваиванием)
Далее уже отдельный внутренний цикл, производит последовательное перемножение элементов.
В этом случае, не будет скачков по ОЗУ и будет максимально использоваться кеш процессора. Т.к. будут перемножаться элементы массива, которые находятся в памяти рядом друг с другом. (что позволит им, попадать в «страницу кеша процессора», и далее обрабатывать их без обращения к памяти)