associação pythonbrasil[11] django zope/plone planet Início Logado como (Entrar)

BenchmarkAdHoc

Benchmark Ad Hoc

Eis alguns resultados de benchmark "ad hoc" comparando PHP, Python, Python com Psyco (compilador JIT), C++ (GCC), Java (Sun e IBM) e Jython (interpretador Python escrito em Java).

Fibonacci 300.000 vezes

O código fonte usado foi:

   1 def f(x):
   2     a, b = 0, 1
   3     n = 0
   4     while n < x:
   5         a, b = b, a+b
   6         n += 1
   7     return a
   8  
   9 for i in range(0, 300000):
  10     f(40)

A versão PHP é muito mais deselegante, porque não suporta a atribuição recíproca. Seria possível fazer algo semelhante usando list() e array(), mas seria mais lento. Então usamos o seguinte código:

function f($x) {
    $a = 0;
    $b = 1;
    $n = 0;
    while ($n < $x) {
        $tmp = $a;
        $a = $b;
        $b = $tmp + $b;
        $n += 1;
    }
    return $a;
}
 
set_time_limit(0);
for ($i = 0; $i < 300000; $i++) {
    f(40);

O set_time_limit() se deve ao fato do PHP morrer após trinta segundos de execução, o que ocorreu nesse teste.

Os resultados foram:

Python

Python com Psyco

PHP

16s

1.1s

31s

Como pode-se notar, a performance do PHP foi sofrível, o Python foi lento e o Python com Psyco teve performance excelente.

Concatenação de strings 300.000 vezes

O código fonte usado foi:

   1 def f():
   2     a = ''
   3     for i in range(300000):
   4         a += 'a'
   5 f()

E a versão PHP:

function f() {
    $a = "";
    for ($i=0; $i < 300000; $i++) {
        $a .= "a";
    }
}
 
f();

Os resultados foram:

Python

Python com Psyco

PHP

4m21s

0.108s

0.771s

O resultado do Python padrão foi deplorável. A performance piora muito quando aumenta-se a quantidade de concatenações. Fazer três vezes o teste com 100.000 concatenações é muito mais rápido que fazer o teste com 300.000 concatenações. Esse foi o resultado mais desastroso do teste.

Por outro lado, o Python com Psyco deu mais uma vez a vitória ao Python, rodando sete vezes mais rápido que o código em PHP.

A vantagem do Psyco sobre o Python normal e sobre o PHP é incrível. O Psyco dá voltas em torno do CPython e do PHP.

Apêndice: C++ contra Psyco

Fizemos comparação de C++ (compilado com g++, então dá um desconto) com Psyco.

Fibonacci

O código C++ usado foi:

int f(int x) {
    int a = 0;
    int b = 1;
    int n = 0;
    int tmp;
    while (n < x) {
            tmp = a;
            a = b;
            b = tmp + b;
            n += 1;
    }
    return a;
}

void main(void) {
    int i;
    for (i=0; i<300000; i++) {
        f(40);
    }
}

O código Python não foi modificado. O código foi compilado com o g++ usando o flag -O6.

C++

Psyco

0.061s

1.1s

Não tem nem comparação a versão C++. Logicamente os tipos usados em C++ são todos primitivos.

Concatenação

O código C++ de concatenação usado foi o seguinte:

#include <string>
 
void f() {
    std::string a = "";
    for (int i=0; i < 300000; i++) {
        a = a + "a";
    }
}

void main(void) {
        f();
}

C++

Psyco

4m33s

0.108s

O Psyco deu voltas em torno da versão C++: o código compilado com g++ demorou 2572 vezes mais que versão Python. Com certeza há algo de errado com o g++/bibliotecas/qualquer coisa que nos impedem de considerar esse resultado típico. A irregularidade apresentada é do mesmo tipo que o Python rodando sem o Psyco: a partir de determinado número de concatenações, o resultado degrada muito rapidamente.

Embora essa versão seja mais parecida com o que a versão Python faz, há uma maneira de otimizar o código que aumenta em muito a performance do código C++. Basta fazer a concatenação usando o operador +=:

a += "a";

C++

Psyco

0.093s

0.108s

Esse resultado é um pouco mais próximo do esperado. A performance do Psyco foi praticamente igual a C++, o que é um feito considerando que a versão Python tem que iniciar o intepretador, importar um módulo, etc. antes de começar a rodar.

Apêndice: Java (e Jython) contra Psyco

Os testes Java foram feitos com um .class compilado pelo JDK 1.4.2 da Sun executado tanto no JRE 1.4.2 da Sun quanto no 1.4.2 da IBM. O teste com GCJ compila o código nativamente. O resultado com Jython foi executado sob a VM da IBM. O Jython é um interpretador Python escrito em Java.

Fibonacci

O código fonte Java usado foi:

public class teste1 {
    public static void main(String args[]) {
        for(int x = 0; x < 300000; ++ x) {
            f(40);
        }
    }

    public static long f(long x) {
        long a = 0;
        long b = 1;
        long n = 0;

        while (n < x) {
            long tmp;
            tmp = a + b;
            a = b;
            b = tmp;
            ++n;
        }

        return a;
    }
}

Java (IBM)

Java (Sun)

Java (GCJ binário)

Psyco

Jython

0.607s

0.427s

0.2s

1.1s

28.7s

O melhor resultado foi o código compilado nativo com GCJ, e o melhor resultado interpretado foi com a VM da Sun. O Jython foi previsivelmente mais lento.

Concatencação

O código fonte do concatenador Java usado foi:

public class teste2 {
    public static void main(String args[]) {
        f();
    }

    public static void f() {
        String a = "";
        for (int j=0; j < 30; j++) {
            for (int i=0; i < 10000; i++) {
                a += "a";
            }
        }
    }
}

Java (IBM)

Java (Sun)

Java (GCJ binário)

Psyco

Jython

16m59s

158m24s

52min

0.108s

58m23s

O resultado com a VM da Sun foi completamente anômalo, tendo demorado mais que duas horas para ser executado. O resultado com a VM da IBM foi terrível, mas muito melhor que a implementação da Sun, e melhor que o código nativo gerado pelo GCJ. O Psyco venceu com uma larguíssima vantagem.

Curiosamente, o Jython rodando sob a VM da IBM foi mais rápido que a versão Java nativa rodando sob a VM da Sun.

O teste foi justo com Java?

Diversas pessoas (ok, duas) apontaram a existência da classe StringBuffer em Java, que otimizaria as concatenações e aceleraria o teste. Salvo melhor juízo, creio que o uso desse tipo seria uma trapaça em favor do Java.

Quando em Python fazemos a = a + "a", estamos fazendo exatamente a mesma coisa que uma concatenação em Java: as strings são objetos, e o resultado da soma é um novo objeto. Foi para isso que std::string foi usado na versão C++. O ponto do benchmark não é concatenar strings, até porque a forma como o teste o faz é tola. O ponto é exercitar a criação de objetos.

Se fossemos usar a maneira mais rápida de executar a tarefa em diferentes linguagens, todas elas teriam ótimos resultados porque criar uma string com 300.000 vezes a letra "a" é uma tarefa simples. Para fazer isso em Python, basta executar "a" * 300000. Isso demora um centésimo de segundo na máquina em que os benchmarks acima foram executados, e o resultado prático é o mesmo do benchmark acima, mas esse teste não é comparável com nenhuma outra linguagem. Em C++ poderíamos alocar previamente 300kb de memória e escrever diretamente a letra "a": o teste seria assutadoramente rápido. O algoritmo para calcular números de Fibonacci nem é o mais rápido que existe.

A preocupação portanto é se as tarefas são equivalentes entre as linguagens, e não se essa é a maneira mais fácil de executá-las.

Um teste em que se comparasse a implementação mais rápida possível de determinadas tarefas nas diferentes linguagens seria interessante, mas seria um teste fundamentalmente diferente desse realizado.

O teste foi justo com Psyco?

O aumento de performance típico por parte do Psyco em um programa mundo real quase certamente seria muito inferior ao apresentado nesses testes. A forma como o Psyco pretende ser usado não é, pelo menos pelo momento, otimizando todo o código Python, e sim otimizando o ponto onde está o problema de performance. Seria incorreto tentar transportar esses dados de benchmark proporcionalmente para o mundo real. Nem o Psyco seria tão rápido, nem o Java seria tão lento.

Em um benchmark paralelo (não publicado aqui pelo menos por enquanto) em que a tarefa era extrair dados de um arquivo e gerar um relatório, a performance do Psyco foi duas vezes maior que o Python rodando sozinho.

Segundo a documentação, os testes que realizamos exercem justamente pontos que o Psyco sabe otimizar muito bem. Note que originalmente inicialmente o objetivo do benchmark (em formato um pouco diferente) era comparar a performance de C++ e Java, portanto os testes não foram escritos explicitamente para beneficiar o Psyco.

O Psyco otimiza a criação de objetos temporários que é o grande problema do teste de concatenação. Não há problemas em nosso benchmark com relação a isso, a estratégia é legítima - para o programador o efeito é o mesmo, e todas as características de objeto estão mantidas. Qualquer interpretador, VM ou compilador poderia otimizar os resultados dessa forma, desde que não alterasse o comportamento dos objetos otimizados. Para o programador não foi necessário conhecer um detalhe de implementação ou usar um objeto de natureza diferente para ter o ganho de performance.

É justo usar um compilador JIT?

Sim, e isso é uma das premissas do teste. Tanto Psyco quanto as duas implementações de Java o fazem. Infelizmente não temos um compilador JIT para PHP (ou para C++ :)), mas se alguém nos apontar algum podemos repetir os testes.

Cache e acelerador para PHP: Turck MMCache

Performance importa?

O programa típico passa 90% do seu tempo esperando entrada do usuário, ou esperando que dados cheguem da rede e do disco. O grande ponto que as linguagens interpretadas provaram foi que o tempo do programador é mais importante do que o tempo do processador. Isso não pode se perder de vista. Exceto em situações muito específicas a "linguagem mais rápida" no sentido computacional não importa. Vale mais a pena que a linguagem seja produtiva. Apesar desse não ter sido o ponto do teste, as implementações acima fazem comentários auto-evidentes sobre produtividade.

Adendo

De: epx
Para: linux@d00dz.org

A versão demorada do C++ também gasta 1/3 do tempo de CPU em kernel mode, o 
que também dá a entender alguma coisa nesse sentido.

Minha hipótese: quando um objeto malloc()eado na áaea brk() é liberado, a área 
*continua* brk()eada e pode voltar a ser utilizada prontamente pelo programa; 
e todos os malloc()s exceto o primeiro são rápidos. 

Já uma área obtida com mmap() é devolvida imediatamente ao kernel assim que 
liberada -- só para ser requisitada logo a seguir, liberada, requisitada ... 
ou seja, inumeras chamadas mmap() seguidas e desnecessárias.

Há como resolver isso em C++ sem mexer em assembler? É claro [...]

Mas fato (surpreendente) é que o psyco consegue otimizar mesmo com esses 
fatores presentes. Possivelmente detectaram o problema acima em algum 
benchmark e criaram outro alocador.

Lembro que em Delphi de vez em quando alguém reclamava do comportamento do 
alocador padrão em relação a fragmentação. Alguns sugeriam para mudar para o 
alocador padrão do Windows, menos fragmentador mas bem mais lento, talvez 
pelos mesmos motivos.

Lembro também de ter lido algo nas especificações de construção de um JVM, 
algo sobre nunca liberar memória brk()eada. Provavelmente para evitar os 
problemas acima ;)

Colaboraram com os testes (mas não são culpados por eles): epx, GustavoNiemeyer, SergioBruder, RudaMoura, mrbrocoli

Comentários de terceiros

* ralobao - Na concatenação de string nós também poderiamos 'trapaçear' assim como o Java, era só fazer uso do módulo array, um exemplo seria:

   1 import array
   2 l = array.array('c','a')
   3 for i in range(30000):
   4     l.append('a')
   5 print l.tostring()

o resultado foi muito mais satisfatorio fazendo uso do módulo array.

abraços e parabens pelo artigo.


GuilhermeManika