Eu estava achando bem divertido traduzir para diferentes linguagens o programa de ver o número de páginas num arquivo PDF. Serve para aprender novas linguagens e me aperfeiçoar naquelas que eu já sei. E, depois de tudo implementado, dá pra comparar o desempenho dos compiladores e descobrir algumas coisas bem interessantes.
Fiz os testes da seguinte forma:
Linguagens usadas: Python, F#, D, Java e C++
Entrada: 261 arquivos PDF.
Saída: o nome de cada arquivo seguido do seu número de páginas.
Execução: comparar o desempenho de passar todos os arquivos pela linha de comando ao processo e iniciar um novo processo para ler cada arquivo. Todos os processos foram executados seqüencialmente, nada em paralelo.
A medição foi feita usando o PowerShell. Resumidamente, os comandos usados foram:
$all_pdfs = ls -filter *.pdf -recurse | foreach { $_.fullname }
measure-command { $all_pdfs | foreach { &$cmd $_ | out-host } }
measure-command { &$cmd @all_pdfs | out-host }
onde a variável $cmd contém uma função que chama o programa desejado (C++, F#, Java, Python, ou D). O código completo do script está no final deste post.
Cada teste foi rodado em seqüência 3 vezes e o menor tempo foi registrado na tabela.
| 1 processo | 1 processo (memória) |
múltiplos processos | |
| C++ | 2.44s (32 bits) 2.03s (64 bits) |
m < 10MiB | 4.73s (32 bits) 3.99s (64 bits) |
| Java | 2.19s (64 bits) | m > 70MiB | 42.79s (64 bits) |
| F# | 3.67s (32 bits) 3.55s (64 bits) ngen: 3.41s (64 bits) |
10MiB < m < 70MiB | 19.19s (32 bits) 72.43s (64 bits) ngen: 14.94s (64 bits) |
| D | 3.82s (32 bits) | m > 70MiB | 6.88s (32 bits) |
| Python | 20.34s (64 bits) | 10MiB < m < 70MiB | 32.5s (64 bits) |
Implementações usadas:
C++
MS VC++ 2010 Express + Windows SDK v7.1
Modo release, otimizações no máximo (/Ox), NDEBUG, buffer security check desligado (/GS-), floating-point rápido (/fp:fast).
Java
JDK 6 update 21 (64 bits)
Nenhuma opção de otimização. Todas as que eu tentei pioravam a performance.
F#
F# 2.0.0.0 + Visual Studio 2010 Shell
Modo release
D
DMD 2.049
Com as opções de otimização: -O -release -inline -noboundscheck
A idéia não era misturar 32 e 64 bits nos testes, mas quando eu vi eu estava com Java e Python em 64 bits (é necessário instalar um pacote separado para cada arquitetura), mas C++ em 32 bits. Sendo que F# é 2-em-1 (32 e 64 bits), baixei o compilador 64 bits para C++ e só então me dei conta que o compilador 64 bits de D ainda não está pronto, ele está sendo finalizado e será lançado em breve. Daí não quis reverter tudo pra 32 bits e deixei assim mesmo.
Python
Python 3.1.2 (64 bits)
Nenhuma opção de otimização, porque os arquivos pyc (Python compilado) e pyo (Python compilado e otimizado) resultaram iguais.
Máquina usada:
Intel Core 2 Quad Q8400 2.66GHz
4GiB de RAM
Windows 7 Ultimate 64 bits (inclusive para rodar as versões 32 bits)
Winamp tocando música durante todos os testes, porque fazer isso em silêncio teria sido muito chato.
Conclusões:
Embora os números já estejam aí em cima, eu gosto de ler uma análise com palavras para sustentar os dados exibidos. Então vou escrever o que concluí.
C++ continua sendo o mais rápido, nenhuma surpresa aqui. Mas eu tinha me esquecido como certos detalhes da linguagem são complicados. Ainda resolvi usar vários recursos do novo C++ (o C++0x ou C++1x) e nem sei se fiz direito. Por usar esses recursos novos, faltou testar com outros compiladores C++, pra ver se o mérito é apenas do VC++ ou se outras implementações também atingem esse desempenho.
Java conseguiu a façanha de marcar o segundo melhor tempo e o segundo pior tempo. Se todos os arquivos são processados pelo mesmo programa o JIT faz um trabalho incrível e chega muito perto da velocidade do C++ (considerando a freqüência que se usa new em Java, é incrível mesmo). Mas se o programa for fechado e reiniciado para cada arquivo, o tempo de carga da JVM pesa muito, e a mesma tarefa que antes levou pouco mais de 2 segundos, passa a levar 42 segundos!!
F#, por ser também uma linguagem compilada com JIT, ficou bem próximo ao Java tanto na velocidade do código gerado pelo JIT quanto pela lentidão da inicialização. Quando eu vi o tempo de mais de 1 minuto, vi que tinha alguma coisa errada e resolvi testar um pouco melhor, pois eu sabia que anteriormente tinha feito outro teste que não tinha sido tão lento.
É que no meu teste anterior eu tinha compilado em 32 bits, e a lentidão se manifesta se compilar pra 64 bits. Será que isso é um bug ou uma característica do funcionamento da máquina virtual e compilador JIT? Imagino que Java também tenha caído nessa, porque fiz outros testes com Java 32 bits que ficaram na faixa de 15 a 22 segundos. Só não coloquei na tabela porque esse teste foi feito com Java 5, em outro computador e com um conjunto menor de arquivos, então não dá pra comparar diretamente, mas dá pra ter uma idéia da ordem de grandeza dos tempos.
Como a CLR (a infraestrutura do C#, F# e Visual Basic .NET) suporta a geração de código nativo com o programa ngen.exe, testei também o seu funcionamento. Como o meu sistema é 64 bits, ele só aceitou gerar executável 64 bits. O resultado foi bom, já que tirou o F# 64 da situação de pior desempenho de inicialização. Não sei por que Java não tem essa opção.
Eu achei que o desempenho do D fosse competir diretamente com o C++, mas na verdade ficou um pouco mais distante do que eu imaginava. O desempenho do Digital Mars D deve ser semelhante ao desempenho do Digital Mars C++, que eu não testei por falta de suporte aos recursos experimentais do novo padrão do C++. Onde D e C++ se destacam juntos é na inicialização (ver a última coluna). Por gerarem código nativo e não usarem nenhuma máquina virtual, executar todo o trabalho em vários processos (seqüenciais, lembrando) não adicionou muita sobrecarga, tendo um desempenho muitíssimo melhor que as outras 3 linguagens nesse teste.
Python corresponde a outra categoria, porque a implementação padrão usa uma máquina virtual sem JIT. Seria bom ter adicionado outra(s) linguagem(ns) nessa categoria. Mas o interessante é que, apesar disso torná-lo bem mais lento que as outras linguagens, ele não ficou em último lugar na coluna da direita por causa dos tropeços das máquinas virtuais do Java e da Microsoft (CLR, CLI, .NET, essas coisas) na inicialização.
Por último, adicionei a coluna do meio, do uso de memória. Enquanto o tempo eu medi com o Measure-Command do PowerShell, a memória foi no olhômetro mesmo, usando o gerenciador de tarefas, por isso os dados não estão tão precisos. Sendo a única implementação sem coletor de lixo, C++ usou muito menos memória que os outros, que deixavam bastante lixo se acumular antes de executar o coletor.
Script em PowerShell pra rodar tudo isso.
Próximo post: premiação.
28 outubro, 2010 às 11:06 |
[...] Link para os testes originais [...]