Estudo de Caso: Portando o Stream para o Android*

Objetivo

Este documento mostra como portar o aplicativo de benchmark Stream para uma plataforma x86 através da criação de um aplicativo Android* que utilize uma biblioteca compartilhada nativa. Consulte http://www.streambench.org para saber as informações mais recentes.

Este artigo serve como um guia para um caso de uso mais avançado de como portar um aplicativo para Android* utilizando o NDK. Especificamente, o benchmark Stream será portado. O Stream existe há algum tempo e estabeleceu um padrão demonstrando a largura de banda de memória do "mundo real" (em oposição às métricas de "largura de banda teórica", que servem mais ao mundo acadêmico, em oposição ao que normalmente é visto na prática).

O Stream fornece ao desenvolvedor uma opção para múltiplas subrotinas. Em aplicativos com única subrotina, a compilação NDK simples é feita sem falhas. No entanto, por padrão, o aplicativo utiliza o OpenMP* como seu método para múltiplas subrotinas. Em outubro de 2011, o Android* ainda não suportava OpenMP* no processo de linkagem. Tendo o desenvolvedor, portanto, que portar o aplicativo para utilizar subrotinas POSIX* ao invés do OpenMP*. O NDK é utilizado então para compilar o aplicativo como uma biblioteca nativa, fornecendo uma infraestrutura para a utilização de subrotinas POSIX* (pthreads).

Pré-requisitos

Este documento assume que o Android* SDK está configurado corretamente para ser utilizado com o Eclipse IDE*. Também assume que a última versão do NDK também está instalada e configurada. O desenvolvedor utiliza o NDK para compilar código C/C++ nativo em uma biblioteca nativa que o aplicativo wrapper do Android* possa utilizar. Neste documento, demonstraremos a compilação e linkagem NDK para o Stream.

Consulte outros documentos da Intel® Developer Zone que descrevem como obter e instalar o Android* SDK e o NDK.

Passos para portar: o arquivo Android.mk

Crie uma nova pasta de projeto para o Stream. Este projeto será utilizado com o Android* NDK (para este exercício, foi utilizado o NDK r6b). Crie uma pasta "jni" dentro da pasta de projeto e coloque nela um arquivo de template Android.mk (ou crie um arquivo do zero). As principais mudanças estão mostradas a seguir, em negrito:

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


Por convenção, LOCAL_MODULE possui um nome começando com "lib", e "stream.c" é considerado o principal arquivo de código fonte do aplicativo.

Agora é o momento de ativar o OpenMP* para o aplicativo na compilação do NDK. Isto implica em ativar tanto o tempo de compilação como o tempo de linkagem. Faça também as seguintes alterações no Android.mk:

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


Tente compilar o projeto com este comando:

ndk-build APP_ABI=x86

Você notará nos resultados da operação acima um erro de compilação porque o NDK não entende a flag -lgomp. A linkagem do OpenMP* não estva ativa no Android* naquele momento. Felizmente, podemos portar o aplicativo para utilizar subrotinas POSIX* (pthreads).

As flags de compilação e linkagem precisam ser alteradas da seguinte forma:



Figura 3.1: Flags de subrotina POSIX*

Passos para portar: Reescrever o Stream para utilizar subrotinas POSIX*

Nota: Não está dentro do escopo deste documento mostrar totalmente os detalhes do códigoconvertido para uso de pthreads. Será dada apenas uma visão geral de alto nível, com foco maior no NDK.

Como ponto de partida, é uma boa idéia apenas adicionar uma flag para suportar pthread no Stream, ao invés de eliminar a infra-estrutura OpenMP*. Aqui, a flag será chamada _PTHREADS. Então, toda vez que um bloco de código para OpenMP* for visto na forma de #pragma omp parallel {...}, a forma semântica equivalente na implementação pthread poderá aparecer da seguinte maneira:

    //<NDK porting>
    //#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;
     }

Figure 4.1: Parallel Loop in POSIX*

Neste caso, THREAD_OFFSET é definida como N / max_threads, onde N é o tamanho do vetor do problema no código fonte Stream. Assim, THREAD_OFFSET é utilizado para permitir que múltiplas subrotinas trabalhem em diferentes regiões de um vetor simultaneamente. thread_id é simplesmente o ID da subrotina dentro deste código e os IDs são armazenados em uma matriz após a criação da subrotina.

Pode ser apropriado, em alguns casos, utilizar mutex locks e unlocks. Com mutex locks e unlocks criei minha própria barreira de sincronização para as subrotinas, uma vez que, semanticamente, eu queria todas as subrotinas sincronizadas após uma seção de código paralelo para imitar a seção paralela da implementação OpenMP*. Nota: a implementação de uma barreira NÃO é trivial devido a todas as nuances de tempo das subrotinas. Na verdade, fica como exercício para o desenvolvedor a implementação porque barreiras são uma opção para manter a conformidade com POSIX*.

Finalmente, uma subrotina foi designada como subrotina "mestre". Essa subrotina foi responsável pela criação das pthreads, pelo tratamento de qualquer computação não-paralela e pela interpretação/exibição dos resultados finais de benchmark.

O desenvolvedor poderá passar para a próxima seção após:
- A implementação de subrotinas POSIX* ter sido concluída
- O desenvolvedor ter verificado a implementação
- A compilação NDK acima ter sido bem sucedida

Após compilar o código de Stream nativo

Agora, o processo típico de chamada de código nativo (compilado) a partir do wrapper Android* (baseado em Java*) pode ser utilizado. Neste caso, o uso de JNI não foi mais difícil que um exemplo básico de "Hello World".

O método de entrada do Stream é simplesmente modificado de modo a ter uma assinatura JNI típica, como a mostrada seguir:



Figura 5.1: Método de entrada Stream modificado

Este cabeçalho assume que o "NativeCaler.java" foi adicionado a um projeto Eclipse* Android*, onde o arquivo de origem é utilizado para chamar o aplicativo Stream através de um comando System.Load(). É claro, o desenvolvedor pode escolher diferentes nomenclaturas da forma mais apropriada. Note também que neste exemplo simples nenhum dos parâmetros do método é utilizado, mas o desenvolvedor pode escolher outra forma, com base no seu aplicativo.

Sumário

Este artigo forneceu uma visão geral de alto nível para assegurar que o aplicativo Stream pode ser compilado corretamente e linkado com suporte a múltiplas subrotinas quando é utilizado como parte de um pacote Android*. Este guia discutiu o processo de portar o código para utilizar subrotinas POSIX* em vez do OpenMP*, no caso de compilação/linkagem com o Android* NDK.

Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.