Портирование приложения Stream на Android*

Цели и задачи

В этом документе показан процесс портирования приложения для тестирования производительности Stream на платформу x86 путем создания приложения Android*, использующего собственную общую библиотеку. Последнюю информацию об этом приложении можно найти на веб-сайте http://www.streambench.org.

Данная статья служит руководством по более сложной задаче портирования приложений на Android* с помощью пакета NDK. В этом конкретном примере будет портировано приложение для тестирования производительности Stream. Приложение Stream уже достаточно долго существует и задает стандарт в области анализа реальной пропускной способности памяти (в отличие от метрик теоретической пропускной способности, которые зачастую отличаются от того, что мы наблюдаем на практике).

Приложение Stream поддерживает многопоточность. При обработке приложения в однопоточном режиме (например, "Tuned") простая компиляция NDK выполняется без сбоя. Однако по умолчанию приложение использует метод многопоточности OpenMP*. По состоянию на октябрь 2011 года Android* еще не поддерживает OpenMP* на этапе редактирования связей. Поэтому разработчик должен портировать приложение, чтобы использовать потоки POSIX*. Затем с помощью пакета NDK он должен скомпилировать приложение в качестве собственной библиотеки, предоставляющей инфраструктуру для использования потоков POSIX* (pthread).

Предварительные требования

В этом документе предполагается, что пакет Android SDK* уже настроен для работы со средой разработки Eclipse* IDE. Кроме того, предполагается, что установлена и настроена последняя версия пакета NDK. Разработчик использует пакет NDK для компиляции собственного кода C/C++ с в собственную библиотеку, которую затем может использовать приложение-оболочка Android*. В этом документе будут показаны компиляция и редактирование связей в пакете NDK для приложения Stream.

Информацию о приобретении и установке пакетов Android* SDK и NDK можно найти в других документах сообщества Intel® Developer Zone.

Этапы портирования: Файл Android.mk

Создайте новую папку проекта для приложения Stream. Этот проект будет использоваться с пакетом Android* NDK (в данном упражнении использовалась версия NDK r6b). Создайте в этой папке папку "jni" и поместите в нее шаблон файла Android.mk (либо создайте этот файл с нуля). Основные изменения ниже выделены полужирным шрифтом:

...
LOCAL_MODULE:= libstream
...
LOCAL_SRC_FILES:= \
stream.c 


По правилам именования имя LOCAL_MODULE начинается с "lib", а "stream.c" считается исходным файлом основного приложения.

Теперь включим поддержку OpenMP* для приложения в системе построения NDK. Она включается как на время компиляции, так и на время редактирования связей. Внесите следующие изменения в файл Android.mk:

LOCAL_LDLIBS := -ldl -llog -lgomp
LOCAL_CFLAGS := -fopenmp 


Попытайтесь построить проект с помощью этой команды:

ndk-build APP_ABI=x86

Вы заметите, что выполненные выше действия приводят к ошибке построения, поскольку пакет NDK не распознает флаг -lgomp. Связывание OpenMP* для Android* пока не поддерживается. Но, к счастью, можно портировать приложение, чтобы использования потоки POSIX* (pthread).

Флаги времени построения и времени редактирования связей необходимо изменить следующим образом:



Рис. 3.1: Флаги потоков POSIX*

Этапы портирования: Изменение кода Stream для использования потоков POSIX*

Примечание. Подробное описание кода, обеспечивающего поддержку pthread, выходит за рамки этого документа. Здесь приводится только общий обзор, в котором основное внимание уделяется пакету NDK.

В качестве отправной точки лучше всего просто добавить для поддержки pthread в приложении Stream флаг make, который заменит нам инфраструктуру OpenMP*. В данном случае флаг make имеет имя _PTHREADS. Затем при каждом появлении блока кода для OpenMP* в формате #pragma omp parallel { … }, семантический эквивалент в виде реализация pthread может выглядеть следующим образом:

    //<Портирование в NDK>
    //#pragma omp parallel for
    for (j=0; j<THREAD_OFFSET; j++)
    {   
        a[j + (THREAD_OFFSET * thread_ID)] = 1.0; 
        b[j+ (THREAD_OFFSET * thread_ID)] = 2.0;
        c[j+ (THREAD_OFFSET * thread_ID)] = 0.0;
     }

Рис. 4.1: Параллельный цикл в POSIX*

В данном примере THREAD_OFFSET определяется как N / MAX_THREADS, где N - размер массива задач в исходном коде Stream. Таким образом, THREAD_OFFSET обеспечивает параллельное выполнение нескольких потоков в различных областях массива. thread_ID - это просто идентификатор входящего в этот код потока; идентификаторы хранятся в массиве после создания потока.

В некоторых случаях могут быть полезны взаимоисключающие блокировки и разблокировки. С помощью взаимоисключающих блокировок и разблокировок я создал свой собственный барьер для синхронизации потоков, поскольку в плане семантики мне хотелось бы синхронизировать вместе все потоки после параллельного участка кода, чтобы имитировать параллельный участок из реализации OpenMP*. Примечание. Реализация барьера потребовала некоторых усилий, поскольку нужно было учитывать различные нюансы, связанные со временем работы потоков. Разработчикам часто приходится заниматься такой реализацией, поскольку барьеры являются одним из способов обеспечения поддержки POSIX*.

Один поток был определен в качестве "главного" потока. Этот поток отвечал за создание потоков pthread, обработку любых непараллельных вычислений, а также за интерпретацию и отображение окончательных результатов тестирования производительности.

Разработчик может перейти к следующему разделу, когда будут выполнены следующие условия:
- Реализован поток POSIX*.
- Реализация проверена разработчиком.
- Успешно выполнена вышеупомянутая компиляция NDK.

Действия после компиляции собственного кода приложения Stream

Теперь можно использовать обычный процесс вызова собственного (откомпилированного) кода из пакета оболочки Android* (на базе Java*). В нашем случае работа со средой JNI была не сложнее обычного примера "Hello World".

Мы просто изменили метод входа Stream, чтобы получить стандартную подпись JNI, как показано на следующем рисунке:



Рис. 5.1: Измененный метод входа Stream

Данный заголовок предполагает, что в проект Android* Eclipse* была добавлена ссылка "NativeCaler.java" и в этом исходном файле вызывается приложение Stream с помощью команды System.Load(). Разработчик, конечно, может выбрать другую подходящую номенклатуру. Обратите также внимание на то, что в этом простом примере не используется ни один из параметров метода, хотя в зависимости от приложения разработчик может их использовать.

Заключение

В этой статье приводится общий обзор того, как обеспечить в приложении Stream правильное выполнение построения и связывания с поддержкой многопоточности, если она используется в пакете Android*. В этом руководстве рассматривается процесс портирования кода для использования потоков POSIX* вместо OpenMP* в случае построения и связывания с помощью пакета Android* NDK.

Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.
Категории: