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

Revisão 10e 2004-06-19 14:22:18

Excluir mensagem

BenchmarkAdHoc

Benchmark Ad Hoc

Eis alguns resultados de benchmark "ad hoc" comparando PHP, Python e Python com psyco:

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();

Note que a versão PHP é mais feia porque PHP é naturalmente mais feio :)

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

0m0.093

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 vs Psyco

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

0.607s

0.427s

0.2s

1.1s

O melhor resultado foi o código compilado nativo com GCJ, e o melhor resultado interpretado foi com a VM da Sun. O Psyco ficou com o pior resultado nesse teste.

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

16m59s

158m24s

45min (aprox.)

0.108s

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 Sun 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.

Comentários finais

Apesar dos resultados mostrarem uma ampla vantagem do Python com Psyco, que empata tecnicamente com C++, tem-se que fazer menção ao fato de que todos os benchmarks são mentirosos.

O que torna esse curioso é que a origem dele (em formato diferente, por iniciativa do epx) foi comparar Java e C++. Não houve um esforço para beneficiar Python.

Logicamente as implementações não são as mais rápidas possível. A versão C++ seria muito mais rápida se alocasse previamente toda a memória e escrevesse diretamente ao invés de usar um objeto, mas queríamos exercitar o sistema de objetos. Nesse ponto PHP foi beneficiado no concatenador por ter um tipo string primitivo ao contrário das outras linguagens.

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 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.

É 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.

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 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


GuilhermeManika