Intel(R) CilkTM Plus — это набор языковых расширений для компилятора Intel(R), помогающий легко и быстро применять распараллеливание и векторизацию для кода C/C++. Для ознакомления с базовыми сведениями о Cilk Plus см. главную страницу этого решения.
Intel Cilk Plus: https://software.intel.com/en-us/intel-cilk-plus
Открытый исходный код Cilk Plus: http://www.cilkplus.org/
Цель этой статьи — описание поддержки Cilk Plus в компиляторе Intel C/C++ для Android. Я расскажу о том, как использовать Cilk Plus для повышения производительности ваших приложений в Android.
Три простых ключевых слова:
Intel Cilk Plus содержит 3 простых ключевых слова, помогающих быстро сделать ваше приложение многопоточным. Это ключевые слова cilk_spawn, cilk_sync и cilk_for, но ими возможности Cilk Plus не ограничиваются. Для использования Cilk Plus необходимо слинковать исполняемую библиотеку Cilk (libcilkrts.so).
Использование Cilk Plus в проекте Android NDK
1. Сначала нужно собрать ваш проект в компиляторе Intel. Нужно добавить приведенный ниже код в файл jni/Application.mk (если такого файла нет, его нужно создать):
NDK_TOOLCHAIN = x86-icc APP_ABI = x86
2. Добавьте подходящую STL в файл Application.mk вашего проекта
APP_STL:=stlport_shared OR APP_STL:=gnustl_static OR APP_STL:=gnustl_shared
Примечания. После задания STL компилятор Intel автоматически слинкуется с выполняемым модулем Cilk Plus. Если не добавить STL, при сборке кода может появиться много ошибок компоновки вида «ссылка не определена».
3. Загрузка выполняемых библиотек в коде Java
System.loadLibrary("gnustl_shared"); System.loadLibrary("cilkrts"); System.loadLibrary("yourJNILib"); OR System.loadLibrary("stlport_shared"); System.loadLibrary("cilkrts"); System.loadLibrary("yourJNILib");
Где yourJNILib — имя вашей библиотеки JNI. Примечание. В последней версии Android исправлена проблема зависимости loadLibrary. В прежних версиях Android при использовании System.loadLibrary, если libyourJNILib.so зависит от libcilkrts.so, а libcilkrts.so зависит от libgnustl_shared.so, необходимо загружать их, как показано выше. Но при тестировании моего приложения выяснилось, что в последней версии Android эта проблема исправлена, зависимые библиотеки загружаются автоматически и нужно загрузить только вашу библиотеку JNI для приведенного выше кода (нужна только одна строка для загрузки yourJNILib).
4. Использование Cilk Plus в коде C/C++
Теперь можно использовать Cilk Plus в вашем коде. В качестве примера возьмем вычисление последовательности Фибоначчи. Ниже приведены две реализации этого алгоритма:
int fib(int n) { if (n == 0 || n == 1) return n; int x = fib(n - 1); int y = fib(n - 2); return x + y; } #include <cilk/cilk.h> int fib_cilk(int n) { if (n == 0 || n == 1) return n; int x = cilk_spawn fib_cilk(n - 1); int y = fib_cilk(n - 2); cilk_sync; return x + y; }
Для использования Cilk Plus необходимо включить заголовки. Для трех простых ключевых слов достаточно включить cilk/cilk.h. Назначение cilk_spawn — создание новой ветви (которое можно рассматривать как задачу Cilk Plus), Cilk Plus будет автоматически управлять фактическими рабочими потоками (которые можно рассматривать как физические потоки), а выполняемый модуль будет сопоставлять ветви с рабочими потоками (количество рабочих потоков по умолчанию совпадает с количеством ядер процессора).
Примечание. В приведенном выше случае код легко понять так: «существует 2 потока, один для выполнения fib_cilk(n-1), а другой для выполнения всего остального кода, т. е. для продолжения кода fib_cilk(n-2)».
Назначение cilk_sync — синхронизация, то есть ожидание завершения всех рабочих потоков, затем продолжение выполнения. Разумеется, для вычисления fib нужно дождаться fib(n-1) и fib(n-2), а затем продолжить выполнение.
Результаты тестирования:
Результаты тестирования таковы: работа кода без Cilk Plus занимает 2831 мс, а код с Cilk Plus работает 10 315 мс. Почему при использовании Cilk Plus код стал работать медленнее? В чем тогда был смысл написания этой статьи?
5. Оптимизация нагрузки
В приведенном выше примере каждый раз при вычислении fib(N) у нас есть два рабочих потока, выполняющих fib(N-1) и fib(N-2). Но если значение N очень маленькое (например, N = 3), то нагрузка fib(N-1) и fib(N-2) будет маленькой, а издержки на создание рабочих потоков и переключение контекста между потоками резко снизят производительность. Такая ситуация возникает постоянно, поэтому издержки будут возникать многократно, а производительность будет намного ниже, чем у кода, не использующего Cilk Plus.
Как решить эту проблему? Нужно пользоваться последовательной обработкой при небольших значениях N. Например:
int fib_cilk_optimize(int n) { if (n == 0 || n == 1) return n; if (n < 20) { int x = fib(n - 1); int y = fib(n - 2); return x + y; } else { int x = cilk_spawn fib_cilk_optimize(n - 1); int y = fib_cilk_optimize(n - 2); cilk_sync; return x + y; } }
Теперь потоки Cilk будут использоваться только для нагрузок, где N >= 20. Результаты тестирования для измененного кода Cilk таковы:
Вот график для приведенных выше результатов:
Примечание. Полный исходный код (проект Android) прилагается в конце этой статьи, его можно загрузить и протестировать на вашем устройстве.
Прочие ресурсы:
Тестовое устройство: Dell Venue 8 (2 ядра, 4 потока в режиме гиперпоточности) (сведения о том, как получить версию образа этого устройства, предназначенную для разработчиков, см. по адресу http://software.intel.com/en-us/android/articles/mobile-development-kit-for-android
Эта статья применима к следующему:
Продукты: Intel(R) INDE; INTEL(R) System Studio
ОС/платформы разработки: Windows (IA-32, Intel(R) 64), Linux* (IA32, Intel(R) 64)
Целевые ОС/платформы: Android* (IA-32)