SemtexGirl

É uma garota dinâmica e de bom gosto, uma garota que explode, que não para e nem aceita um não como resposta.

Aumentando o desempenho ao acessar arrays e os membros de um objeto

Ao desenvolvermos uma aplicação em JNI o acesso aos membros de uma classe é normalmente realizado através das próprias funções JNI, em vez de permitir acesso direto aos membros nativos. Este procedimento tem uma vantagem e um inconveniente:

• Vantagem: Os objetos apontados por referencias são visto de forma simples pelo programador.
• Inconveniente: Impacto no desempenho

Este impacto no desempenho se vê atenuado com a utilização de métodos nativos apenas para realizar funções não triviais, é por este motivo que não costumamos usa-los para realizar operações triviais, exceto é claro em exemplos como neste artigo.

Desempenho acessando arrays

Quando trabalhamos com arrays múltiplos não é viável o uso de função para acessarmos cada elemento de um array, no intuito de resolver este problema foi desenvolvida uma solução chamada ‘pinning’, que consiste em um método nativo que pede a maquina virtual que não remova da memória o conteúdo de um array, desta forma o método nativo pode obter o ponteiro que aponta diretamente para o conteúdo de um array. Esta solução, entretanto, tem duas implicações:

• O garbage coletor deve suportar ‘pinning’. Em muitas implementações é inviável utilizar ‘pinning’ pois complica o garbage coletor e induz � fragmentação da memória.
• A máquina virtual deve posicionar as primitivas contiguamente na memória. Embora esta seja a disposição natural para array de tipos primitivos, os arrays booleanos podem ser implementados como empacotados e desempacotados. Uma disposição booleana empacotado usa um bit para cada elemento, visto que desempacotado usa tipicamente um byte para cada elemento. Conseqüentemente, o código nativo que confia na disposição exata dos arrays booleanos perdera sua portabilidade.

E através das funções do tipo GetArrayElements() podemos apenas obter ponteiros de tipo primitivos de dados, os arrays de objetos devem ser acessados individualmente usando GetObjectArrayElement().

Ainda assim GetArrayElements() não nos garante a obtenção do ponteiro do array original uma vez que ele cria uma cópia em outra área de memória, em resumo esta decisão depende de:

Que o garbage colector suporte “pinning”, no qual implica que o objeto não pode ser liberado ainda que não seja uma referência Java e para que o JNI o libere é preciso executar a função ReleaseArrayElements().

A distribuição dos elementos de um array na memória, assim como o seu tamanho sejam os mesmos do C.

De outra forma o array será copiado para outra área de memória e será formatado convenientemente.

Atento ao fato de que só quando chamamos a função ReleaseArrayElements() que a máquina virtual finaliza o pinning, deixando livre uma cópia do array.

Outra solução que foi incorporada ao JSDK 1.2 as funções GetPrimitiveArrayCritical() e ReleasePrimitiveArrayCritical(), ao uso destas funções são recomendadas sempre que uma thread não:

• Possuir um processo de tempo indefinido
• Chamar outras funções JNI
• Realizar uma operação “thread sleep” (suspensão da execução da thread por um tempo determinado)

Cumprindo estas condições a máquina virtual pode desativar temporariamente o garbage colector enquanto o método nativo acessa diretamente o array, sem que a máquina virtual use pinning. Isto aumenta a possibilidade da máquina virtual retornar o array original em vez de uma cópia.

  1. /* parametros.java */
  2.  
  3. public class Parametros {
  4.  
  5. private native int soma(int a, int b);
  6. private native String texto(String prompt);
  7. private native int[] somaArrays(int[] A, int[] B);
  8.  
  9. public static void main(String[] args) {
  10. Parametros parametros = new Parametros();
  11. System.out.println("4+6=" + parametros.soma(4, 6));
  12. String string = parametros.texto("Digite algum texto");
  13. System.out.println("O texto digitado foi:" + string);
  14. int[] A = { 1, 2, 3 };
  15. int[] B = { 4, 5, 6 };
  16. int[] C = parametros.somaArrays(A, B);
  17. if (C == null) {
  18. System.out.println("Não foi possível realizar a soma.");
  19. } else {
  20. System.out.println("O resultado da soma dos arrays é:");
  21. for (int i = 0; i < C.length; i++) {
  22. System.out.println(C[i] + " ");
  23. }
  24. System.out.println();
  25. }
  26. }
  27.  
  28. static {
  29. System.loadLibrary("parametros");
  30. }
  31. }
  32.  

Listagem 13: Código da classe Parametros.java

Implementação em C.

  1. /* parametros.cpp */
  2.  
  3. JNIEXPORT jintArray JNICALL Java_Parametros_somaArrays(JNIEnv* env,
  4. jobject obj, jintArray arrayA, jintArray arrayB){
  5.  
  6. jintArray arrayC;
  7. jint* A;
  8. jint* B;
  9. jint* C;
  10. jsize i;
  11. //Pegamos o tamanho de um dos arrays e comparamos
  12. jsize tamanho = (*env)->GetArrayLength(env,ArrayA);
  13. if(tamanho!=(*env)->GetArrayLength(env,arrayB))
  14. return NULL; //tamanho dos arrays são diferentes.
  15.  
  16. //criação do array de retorno
  17. arrayC = (*env)->NewIntArray(env, tamanho);
  18.  
  19. A = (jint*)(*env)->GetPrimitiveArrayCritical(env, arrayA, NULL);
  20. B = (jint*)(*env)->GetPrimitiveArrayCritical(env, arrayB, NULL);
  21. A = (jint*)(*env)->GetPrimitiveArrayCritical(env, arrayC, NULL);
  22.  
  23. for (i=0; i < tamanho; i++)
  24. C[i] = A[i]+B[i];
  25.  
  26. (*env)->ReleasePrimitiveArrayCritical(env, ArrayA,A, JNI_ABORT);
  27. (*env)->ReleasePrimitiveArrayCritical(env, ArrayB,B, JNI_ABORT);
  28. (*env)->ReleasePrimitiveArrayCritical(env, ArrayA,A, 0);
  29.  
  30. return C;
  31. }
  32.  

Listagem 14: Código nativo parametros.cpp

Por ultimo devo dizer que JNI garante o acesso concorrente ao array por várias threads, uma vez que o JNI mantém um controlador de pinnings, para que uma thread não finalize o pinning de um array que outra thread esta usando.

Desempenho ao acessar os membros de um objeto

Para obtermos o fieldID ou o methodID realizamos uma busca simbólica em uma base de nomes dos membros, o que é relativamente custoso. Uma forma de diminuir este custo quando houver necessidade de se busca-los mais de uma vez é cachear-los.

Para cachear podemos usar duas técnicas:

1.Cachear em variáveis estáticas de uma função nativa, como por exemplo a função abaixo:

  1. JNIEXPORT void JNICALL Java_MyClass_accessField(JNIEnv *env, jobject obj){
  2.  
  3. static jfieldID fid_s = NULL; /* cacheando field ID para s */
  4. jclass cls = (*env)->GetObjectClass(env, obj);
  5. jstring jstr;
  6. const char *str;
  7. if (fid_s == NULL) {
  8. fid_s = (*env)->GetFieldID(env, cls, "s","Ljava/lang/String;");
  9. if (fid_s == NULL) {
  10. return;
  11. }
  12. }
  13. printf("Aplicação em C:\n");
  14. jstr = (*env)->GetObjectField(env, obj, fid_s);
  15. str = (*env)->GetStringUTFChars(env, jstr, NULL);
  16. if (str == NULL) {
  17. return;
  18. }
  19. printf(" c.s = \"%s\"\n", str);
  20. (*env)->ReleaseStringUTFChars(env, jstr, str);
  21. jstr = (*env)->NewStringUTF(env, "123");
  22. if (jstr == NULL) {
  23. return;
  24. }
  25. (*env)->SetObjectField(env, obj, fid_s, jstr);
  26. }

Listagem 15: Código nativo para cache em variáveis estáticas

2.Cachear ao inicializar uma classe, isso quer dizer que em um bloco estático podemos chamar uma função que inicializa todos os atributos e métodos que vamos precisar.

  1. public class MyClass {
  2.  
  3. private static native void initIDs();
  4. private native void nativeMethod();
  5.  
  6. private void callback(){
  7. System.out.println("Aplicação Java");
  8. }
  9.  
  10. public static void main(String[] args) {
  11. MyClass call = new MyClass();
  12. call.nativeMethod();
  13. }
  14.  
  15. static{
  16. System.loadLibrary("myclass");
  17. initIDs();
  18. }
  19. }
  20.  

Listagem 16: Código da classe MyClass, cache ao inicializar a classe.

Implementação em C.

  1. jmethodID MID_MyClass_callback;
  2.  
  3. JNIEXPORT void JNICALL Java_MyClass_initIDs(JNIEnv *env, jclass cls){
  4. MID_ MyClass_callback = (*env)->GetMethodID(env, cls, "callback", "()V");
  5. }
  6.  
  7. JNIEXPORT void JNICALL Java_ MyClass_nativeMethod(JNIEnv *env, jobject obj){
  8. printf("Aplicação C\n");
  9. (*env)->CallVoidMethod(env, obj,MID_MyClass_callback);
  10. }
  11.  

Listagem 17: Código nativo que implementa o cache na inicialização da classe

E logo initIDs() guarda os IDs em variáveis globais.
Das duas técnicas a mais recomendada é a segunda por varias razões:

1.Os IDs se tornam inválidos logo que a classe é descarregada. Conforme o class loader descarrega a classe e ao se carregar a classe não se atualizam os IDs, na primeira solução porem o mesmo não acontece.

2.É relativamente mais rápido uma vez que carregados os IDs não teremos que voltar a comprovar o valor do ID com:

if (fid_s == NULL) {}

Diferenças de custo entre os objetos nativos e os métodos Java

Neste tópico vamos comentar rapidamente qual o custo de se realizar uma chamada Java-nativo, nativo-Java (chamada callback), comparado com uma chamada java-java.

Uma chamada Java-nativo é mais lenta que uma chamada java-java porque:

1.A máquina virtual tem que liberar as referências locais pra depois chamar o método nativo.
2.A máquina virtual tem que buscar uma função dinâmica em sua biblioteca.
3.A máquina virtual muitas vezes pode chamar in-line quando um método Java chama outro método Java.

Estima-se que o custo é de duas a três vezes superior ao de uma chamada java-java.
Estima-se também que o custo de uma chamada nativo-Java (callback) é duas ou três vezes superior a uma chamada Java-Java. O principal custo agregado esta em:

1.Obtenção do fieldID e do methodID
2.E no acesso aos atributos através de funções, enquanto que a virtual machine pode acessá-los diretamente.

Para Saber Mais
Indicado pela Sun como fonte definitiva de informações sobre JNI.
LIANG, Sheng. The Java Native Interface - Programmers Guide and Specification.
Massachusetts: ADDISON-WESLEY, 1999. ISBN 0-201-32577-2.
Pode ser baixado em:

http://java.sun.com/docs/books/jni/download/jni.pdf

Considerações Finais

Neste artigo apresentamos uma visão geral de como se escrever aplicações híbridas envolvendo Java, JNI e bibliotecas dinâmicas nativas criadas a partir de código C. Abordamos algumas técnicas que podem ser utilizadas para facilitar a integração de sistemas legados. E, ainda uma breve visão de custo e melhoria de desempenho.

Como podemos perceber o JNI é uma ferramenta poderosa, contudo, é preciso estar ciente que uma aplicação Java chamando código nativo compromete a portabilidade e a segurança da aplicação. Pois, embora o bytecode Java compilado permaneça altamente portável, o código nativo deve ser recompilado para cada plataforma na qual se pretenda executar a aplicação.

Referencias

LIANG, Sheng. The Java Native Interface - Programmers Guide and Specification.
Massachusetts: ADDISON-WESLEY, 1999. ISBN 0-201-32577-2.

LINDHOLM, Tim;YELLIN, Frank. Java The JavaTM Virtual Machine Specification.
1999. Ultimo acesso em 17 de Julho de 2006. Disponível em:
< http://java.sun.com/docs/books/vmspec/2nd-edition/html/VMSpecTOC.doc.html>

MEAD, Matthew. Programmming in C/C++ with the Java Native Interface.
1998. Ultimo acesso em 17 de Julho de 2006. Disponível em:
< http://www.pacifier.com/~mmead/jni/cs510jip/index.html >.

SUN MICROSYSTEMS. Java Native Interface Specification.
2003. Ultimo acesso em 17 de Julho de 2006. Disponível em:

SUN MICROSYSTEMS. javap - The Java Class File Disassembler.
2002. Ultimo acesso em 17 de Julho de 2006. Disponível em:
< http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/javap.html>

SUN MICROSYSTEMS. javah - C Header and Stub File Generator.
2002. Ultimo acesso em 17 de Julho de 2006. Disponível em:
< http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/javah.html>

1 comment

1 Comment so far

  1. Luca Bastos maio 11th, 2007 12:57

    Olá

    Gostei do blog, só precisa agitar mais.

    Já que gosta de JNI, dê uma olhada no antigo projeto JNA de 1999 que agora está sendo reavivado dentro da atual tentativa desesperada da Sun de recuperar o terreno perdido.

    https://jna.dev.java.net/