11761
Comentário:
|
11870
|
Deleções são marcadas assim. | Adições são marcadas assim. |
Linha 4: | Linha 4: |
Eis alguns resultados de benchmark "ad hoc" comparando PHP, Python e Python com psyco: | 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). |
Linha 228: | Linha 228: |
|| '''Java (IBM)''' || '''Java (Sun)''' || ''' Java (GCJ binário)''' || '''Psyco''' || '''Jython''' | || '''Java (IBM)''' || '''Java (Sun)''' || ''' Java (GCJ binário)''' || '''Psyco''' || '''Jython''' || |
Linha 298: | Linha 298: |
Colaboraram com os testes (mas não são culpados por eles): epx, GustavoNiemeyer, SergioBruder, RudaMoura | Colaboraram com os testes (mas não são culpados por eles): epx, GustavoNiemeyer, SergioBruder, RudaMoura, mrbrocoli |
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:
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:
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 |
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 (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:
1 public class teste1 {
2 public static void main(String args[]) {
3 for(int x = 0; x < 300000; ++ x) {
4 f(40);
5 }
6 }
7
8 public static long f(long x) {
9 long a = 0;
10 long b = 1;
11 long n = 0;
12
13 while (n < x) {
14 long tmp;
15 tmp = a + b;
16 a = b;
17 b = tmp;
18 ++n;
19 }
20
21 return a;
22 }
23 }
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:
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 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.
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