Dicas de desempenho

Este documento aborda principalmente as micro-otimizações que podem melhorar o desempenho geral do app quando combinadas, mas é improvável que essas alterações resultem em efeitos de desempenho significativos. Escolher as estruturas de dados e os algoritmos corretos deve ser sempre sua prioridade, mas isso está fora do escopo deste documento. Use as dicas deste documento como práticas gerais de programação que você pode incorporar nos seus hábitos para a eficiência geral do código.

Existem duas regras básicas para escrever um código eficiente:

  • Não faça trabalhos desnecessários.
  • Não aloque memória se puder evitar.

Um dos problemas mais complicados que você enfrentará ao fazer uma micro-otimização em um app Android é garantir a execução dele em vários tipos de hardware. Diferentes versões da VM em execução em diferentes processadores que funcionam em velocidades variadas. Nem sempre você pode simplesmente dizer "o dispositivo X é F vezes mais rápido/mais lento que o dispositivo Y" e dimensionar seus resultados de um dispositivo para outros. Em particular, a medição no emulador informa muito pouco sobre o desempenho em qualquer dispositivo. Existem também grandes diferenças entre dispositivos com e sem um JIT: o melhor código para um dispositivo com um JIT nem sempre é o melhor código para um dispositivo sem ele.

Para que seu app tenha bom desempenho em uma grande variedade de dispositivos, garanta que seu código seja eficiente em todos os níveis e otimize seu desempenho de maneira agressiva.

Evitar a criação de objetos desnecessários

A criação de objetos nunca é gratuita. Um coletor de lixo geracional com pools de alocação por linha de execução para objetos temporários pode deixar a alocação mais barata, mas alocar memória é sempre mais caro do que não fazer isso.

À medida que aloca mais objetos no app, você força uma coleta de lixo periódica, criando pequenos "soluços" na experiência do usuário. O coletor de lixo simultâneo introduzido no Android 2.3 ajuda, mas evite sempre os trabalhos desnecessários.

Portanto, evite criar instâncias de objeto desnecessárias. Veja alguns exemplos do que pode ajudar:

  • Se você tiver um método que retorna uma string e souber que o resultado sempre será anexado a um StringBuffer, altere sua assinatura e implementação para que a função seja anexada diretamente, em vez de criar um objeto temporário de curta duração.
  • Ao extrair strings de um conjunto de dados de entrada, retorne uma substring dos dados originais, em vez de criar uma cópia. Você criará um novo objeto String, mas ele compartilhará o char[] com os dados. O problema é que, se você seguir essa linha e estiver usando apenas uma pequena parte da entrada original, manterá tudo isso na memória de qualquer maneira.

Uma ideia um pouco mais radical é dividir matrizes multidimensionais em matrizes unidimensionais únicas paralelas:

  • Uma matriz de ints é muito melhor que uma matriz de objetos Integer. No entanto, isso também generaliza o fato de que duas matrizes paralelas de inteiros também são muito mais eficientes que uma matriz de objetos (int,int). O mesmo vale para qualquer combinação de tipos primitivos.
  • Se você precisar implementar um contêiner que armazene tuplas de objetos (Foo,Bar), tente lembrar que duas matrizes Foo[] e Bar[] paralelas são muito melhores do que uma única matriz de objetos (Foo,Bar) personalizados. A única exceção ocorre, evidentemente, quando você está projetando uma API para outro código acessar. Nesses casos, normalmente é melhor comprometer um pouco a velocidade para ter um bom design de API. Mas no próprio código interno, procure ser o mais eficiente possível.

De modo geral, evite criar objetos temporários de curta duração, se possível. A criação de menos objetos diminui a frequência da coleta de lixo, o que afeta diretamente a experiência do usuário.

Preferir o estático ao virtual

Se você não precisar acessar os campos de um objeto, use um método estático. As invocações serão cerca de 15% a 20% mais rápidas. Isso também é recomendado porque você pode usar a assinatura do método para informar que a ação de chamá-lo não pode alterar o estado do objeto.

Usar final estático para constantes

Veja a seguinte declaração no topo de uma classe:

    static int intVal = 42;
    static String strVal = "Hello, world!";
    

O compilador gera um método inicializador de classe, chamado <clinit>, que é executado quando a classe é usada pela primeira vez. O método armazena o valor 42 em intVal e extrai uma referência da tabela de constantes de string classfile para strVal. Quando são referenciados posteriormente, esses valores são acessados com pesquisas de campo.

Podemos melhorar o caso com a palavra-chave "final":

    static final int intVal = 42;
    static final String strVal = "Hello, world!";
    

A classe não requer mais um método <clinit> porque as constantes entram em inicializadores de campo estático no arquivo dex. O código que se refere a intVal usará o valor inteiro 42 diretamente, e os acessos a strVal usarão uma instrução "constante de string" relativamente econômica em vez de uma pesquisa de campo.

Observação: essa otimização é válida apenas para tipos primitivos e constantes String, não para tipos de referência arbitrários. Ainda assim, é uma prática recomendada declarar constantes static final sempre que possível.

Usar a sintaxe do loop "for" aprimorado

O loop for aprimorado (também conhecido como loop "for-each") pode ser usado em coleções que implementam a interface Iterable e em matrizes. Com as coleções, um iterador é alocado para fazer chamadas de interface para hasNext() e next(). Com uma ArrayList, um loop contabilizado escrito à mão é cerca de três vezes mais rápido (com ou sem JIT). No entanto, para outras coleções, a sintaxe do loop "for" aprimorado será exatamente equivalente ao uso explícito do iterador.

Existem várias alternativas para iterar por uma matriz:

    static class Foo {
        int splat;
    }

    Foo[] array = ...

    public void zero() {
        int sum = 0;
        for (int i = 0; i < array.length; ++i) {
            sum += array[i].splat;
        }
    }

    public void one() {
        int sum = 0;
        Foo[] localArray = array;
        int len = localArray.length;

        for (int i = 0; i < len; ++i) {
            sum += localArray[i].splat;
        }
    }

    public void two() {
        int sum = 0;
        for (Foo a : array) {
            sum += a.splat;
        }
    }
    

zero() é mais lento porque o JIT ainda não pode otimizar o custo de capturar o tamanho da matriz uma vez para cada iteração por meio do loop.

one() é mais rápido. Ele extrai tudo para variáveis locais, evitando as pesquisas. Apenas o tamanho da matriz oferece um benefício de desempenho.

two() é o mais rápido para dispositivos sem JIT e não pode ser diferenciado de one() para dispositivos com JIT. Ele usa a sintaxe do loop "for" aprimorado introduzida na versão 1.5 da linguagem de programação Java.

Portanto, use o loop for aprimorado por padrão, mas considere um loop contabilizado escrito à mão para a iteração de ArrayList com desempenho crítico.

Dica: veja também Effective Java de Josh Bloch, item 46.

Considerar o pacote em vez de acesso privado com classes internas privadas

Veja a seguinte definição de classe:

    public class Foo {
        private class Inner {
            void stuff() {
                Foo.this.doStuff(Foo.this.mValue);
            }
        }

        private int mValue;

        public void run() {
            Inner in = new Inner();
            mValue = 27;
            in.stuff();
        }

        private void doStuff(int value) {
            System.out.println("Value is " + value);
        }
    }

O importante aqui é definirmos uma classe interna privada (Foo$Inner) que acesse diretamente um método privado e um campo de instância privado na classe externa. Isso está de acordo com a lei, e o código imprime "O valor é 27" conforme esperado.

O problema é que a VM considera o acesso direto aos membros particulares de Foo de Foo$Inner como ilegal porque Foo e Foo$Inner são classes diferentes, mesmo que a linguagem Java permita que uma classe interna acesse os membros privados de uma classe externa. Para preencher a lacuna, o compilador gera alguns métodos sintéticos:

    /*package*/ static int Foo.access$100(Foo foo) {
        return foo.mValue;
    }
    /*package*/ static void Foo.access$200(Foo foo, int value) {
        foo.doStuff(value);
    }

O código da classe interna chama esses métodos estáticos sempre que precisa acessar o campo mValue ou invocar o método doStuff() na classe externa. Isso quer dizer que o código acima realmente se resume aos casos em que você acessa campos de membro por meio de métodos de acesso. Anteriormente, falamos sobre como os acessadores são mais lentos que o acesso direto ao campo. Sendo assim, esse é um exemplo de uma determinada expressão que resulta em uma queda de desempenho "invisível".

Se você estiver usando um código como esse em um ponto de acesso de desempenho, poderá evitar a sobrecarga declarando que os campos e métodos acessados por classes internas têm acesso ao pacote, em vez de acesso privado. No entanto, isso significa que os campos podem ser acessados diretamente por outras classes no mesmo pacote. Sendo assim, você não deve usar isso em uma API pública.

Evitar o uso de ponto flutuante

Como regra geral, o ponto flutuante é cerca de duas vezes mais lento que o número inteiro em dispositivos com Android.

Em termos de velocidade, não há diferença entre float e double no hardware mais moderno. Quanto ao espaço, double é duas vezes maior. Assim como acontece com computadores, supondo que o espaço não seja um problema, prefira double a float.

Além disso, mesmo para números inteiros, alguns processadores têm multiplicação de hardware, mas não têm divisão de hardware. Nesses casos, as operações módulo e de divisão de inteiros são realizadas no software, algo para se pensar se você estiver projetando uma tabela de hash ou fazendo muitos cálculos.

Conhecer e usar as bibliotecas

Além de todos os motivos comuns para escolher o código da biblioteca em vez do seu, lembre-se de que o sistema tem a liberdade de substituir chamadas para métodos de biblioteca por um conversor codificado manualmente, o que pode ser mais interessante do que o melhor código que o JIT pode produzir para o Java equivalente. O exemplo típico disso é String.indexOf() e as APIs relacionadas, que a Dalvik substitui por um intrínseco embutido. Da mesma forma, o método System.arraycopy() é cerca de nove vezes mais rápido do que um loop codificado manualmente em um Nexus One com o JIT.

Dica: veja também Effective Java de Josh Bloch, item 47.

Usar os métodos nativos com cuidado

Desenvolver seu app com código nativo usando o Android NDK não é necessariamente mais eficaz do que programar com a linguagem Java. Por um lado, há um custo associado à transição nativa do Java, e o JIT não oferece otimização além desses limites. Se você estiver alocando recursos nativos (memória no heap nativo, descritores de arquivo ou qualquer outro), pode ser significativamente mais difícil organizar uma coleção oportuna desses recursos. Você também precisa compilar seu código para cada arquitetura em que o app será executado (em vez de confiar na presença de um JIT). Talvez seja necessário inclusive compilar várias versões para o que você considera ser a mesma arquitetura: o código nativo compilado para o processador ARM no G1 não consegue tirar pleno proveito do ARM no Nexus One, e o código compilado para o ARM no Nexus One não será executado no ARM no G1.

O código nativo é útil principalmente quando você tem uma codebase nativa já existente que quer transferir para o Android, não para "acelerar" partes do seu app Android escritas com a linguagem Java.

Se precisar usar código nativo, leia nossas Dicas sobre a JNI.

Dica: veja também Effective Java de Josh Bloch, item 54.

Mitos de desempenho

Em dispositivos sem um JIT, é verdade que a invocação de métodos por meio de uma variável com um tipo exato em vez de uma interface é um pouco mais eficiente. Portanto, por exemplo, era mais barato invocar métodos em um HashMap map do que em um Map map, embora em ambos os casos o mapa fosse um HashMap. A diferença real era 6% mais lenta. Além disso, o JIT torna os dois efetivamente indistinguíveis.

Em dispositivos sem JIT, a opção de armazenar acessos a campo em cache é cerca de 20% mais rápida do que acessar o campo várias vezes. Com um JIT, os custos de acesso ao campo são praticamente os mesmos do acesso local. Portanto, essa não é uma otimização que vale a pena, a não ser que você ache que isso facilita a leitura do código. Isso também é válido para os campos finais, estáticos e finais estáticos.

Avaliar sempre

Antes de começar a otimizar, verifique se você tem algum problema que precisa resolver. Verifique se é possível avaliar com precisão o desempenho já existente ou se não será possível avaliar o benefício das alternativas testadas.

Você também pode achar o Traceview útil para criar perfis, mas é importante saber que ele atualmente desativa o JIT, o que pode fazer com que ele atribua erroneamente o tempo de codificação que o JIT pode recuperar. Isso é especialmente importante depois de fazer as alterações sugeridas pelos dados do Traceview, para garantir que o código resultante seja de fato executado mais rapidamente quando estiver sem o Traceview.

Para receber mais ajuda com a criação de perfis e a depuração dos seus apps, consulte os seguintes documentos: