29 de mai. de 2013

PHP: obtenha a melhor performance ao percorrer arrays

Os que me conhecem sabem que não sou grande fã do PHP, muito pelo contrário, é uma das linguagens mais deselegantes que conheço, contudo programo em PHP sempre que necessário e não tenho medo de sujar as mãos :)

Para ninguém dizer que não contribuo com a comunidade PHP trouxe este post, que partiu de um tópico na lista do curso de Análise e Desenvolvimento de Sistemas no IFRS, que fala a respeito de como iterar arrays no PHP obtendo a melhor performance.

Quando digo performance, não pense que seus programas PHP irão dobrar de velocidade, na verdade são micro-otimizações, ou seja, reduzem o tempo de resposta em milisegundos até décimos de milisegundos, mas que na quantidade de acessos simultâneos, por exemplo um Web Site com muitos page views, pode trazer ganhos significativos.

Indo ao ponto, os arrays no PHP, ou arrays associativos, não são como os arrays primitivos de outras linguagens, eles são na verdade um map, ou dicionário, e são armazenados como hash tables através de bindings em C. Dito isso, há características interessantes que trazem alterações na performance dependendo da maneira como é feito, por exemplo, este modo de iterar um array tem pouca performance:

$elementos = array();
 
for ($i = 0; $i < 1000000; $i++) $elementos[$i] = rand();    

$inicio = microtime(true);

for ($i = 0; $i < count($elementos); $i++) {
    $elementos[$i];
}    

$fim = microtime(true);

echo ($fim - $inicio);
Esta implementação executa em 0.366 segundos no meu humilde notebook que é muito tempo se comparada a este implementação a seguir que executa em 0.106 segundos:
$elementos = array();
 
for ($i = 0; $i < 1000000; $i++) $elementos[$i] = rand();    

$inicio = microtime(true);

for ($i = 0, $count = count($elementos); $i < $count; $i++) {
    $elementos[$i];
}    

$fim = microtime(true);

echo ($fim - $inicio);
A grande diferença está na função count, a qual é custosa em tempo, logo é melhor fazê-la uma única vez, na inicialização do for, em vez de introduzi-la na condição de saída. Mas não acabou ainda, esta implementação é ainda melhor (0.085 segundos):
$elementos = array();
 
for ($i = 0; $i < 1000000; $i++) $elementos[$i] = rand();    

$inicio = microtime(true);

foreach ($elementos as $e) {
    $e;
}   

$fim = microtime(true);

echo ($fim - $inicio);

O foreach é otimizado para atravessar o array através da estrutura de dados utilizada, uma lista ligada (linked list).

Existem outras micro-otimizações que trarei num próximo post.

24 de mai. de 2013

Refatoração: substitua exceção por teste

Durante uma aula surgiu uma implementação feita por um aluno, créditos ao Matheus Cezar, onde se encaixa uma refatoração bem interessante: substituir exceção por teste.

A crônica é a seguinte: considere um método que retorne um elemento de uma coleção pelo índice ou nulo caso não exista o dado elemento. Um código cobaia pode ser visto a seguir.

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        // implementação aqui
    }
}

Existem vários modos de cumprir a API do método, uma delas é introduzindo um mau cheiro no código, utilizando a captura de exceções como desvio condicional. É bem simples, veja a seguir:

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
        
    public static void main(String[] args) {
        System.out.println(getOpcao(9));
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        try {
            return opcoes[numero];
        } catch (ArrayIndexOutOfBoundsException e) {
            return null;
        }
        
    }
}

Esta é uma implementação intuitiva a primeira vista, note que ela resolve o problema, ou seja, neste exemplo é impresso null, pois como não há o índice 9 o código opcoes.get(numero) lança a exceção ArrayIndexOutOfBoundsException que é capturada e suprimida, retornando null na situações excepcionais.

Existem um detalhe importante a respeito de exceções, elas introduzem um overhead, já que cada vez que acontecem, além do bloco condicional um objeto é criado para representar a exceção.

O que eu quero dizer é que esta implementação, baseada na captura da exceção, tem baixa performance se comparada com a realização de um teste, que é a refatoração proposta e inclusive documentada no livro do Fowler.

Escrevi um pequeno benchmark para instrumentar este código, veja a seguir:

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        long inicio = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            getOpcao(9);
        }
        System.out.println((System.currentTimeMillis() - inicio) + "ms");
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        try {
            return opcoes.get[numero];
        } catch (ArrayIndexOutOfBoundsException e) {
            return null;
        }
        
    }
}
O tempo decorrido para executar 10000 (dez mil) consultas é de 110ms no meu modesto notebook equipado com um Intel Pentium Dual Core. A refatoração a seguir torna o código mais claro e também mais rápido, é a substituição de exceção por teste, veja a seguir:
public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        long inicio = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            System.out.println(getOpcao(9));
        }
        System.out.println((System.currentTimeMillis() - inicio) + "ms");
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        if (numero >= 0 && numero < opcoes.length)        
            return opcoes[numero];        
        return null;
    }
}

Esta implementação executa em 5ms em média, bem melhor do que os 110ms anteriores, não? A mecânica é simples, em vez de confiar no catch nós fazemos um teste para verificar a validade do parâmetro.

É isso, deixe seu comentário se quiser.