How to tell…

How to tell if your cat is planning to kill youÉ, é importantíssimo saber se teu gato está planejando te matar. E não te esquece de conferir se o teu cachorro é retartado.

Anúncios

Premiação

Tendo os resultados do teste de desempenho, escolhi dar alguns “prêmios” para as linguagens.

Prêmio Rapidez com consistência

C++

Prêmio Lentidão com consistência

Python. É lento, mas pelo menos inicializa num tempo razoável. Além disso, é a única linguagem com tipagem dinâmica dos testes, então não dá bem pra comparar.

Prêmio Lentidão de inicialização

As linguagens com máquinas virtuais e JIT: Java e F#, principalmente na versão 64 bits. Perderam até pro Python.

Acho melhor tirarem aqueles loops vazios até um milhão que puseram lá :-P

Prêmio Concisão

Python e F#. Apesar de eu não ter me preocupado com coisas como linhas em branco e comentários, essas duas linguagens tiveram os códigos mais concisos (quase empatadas). As duas usam da indentação para delimitar blocos.

Prêmio Verbosidade

PL/SQL. A implementação em PL/SQL não estava nos testes porque ela funciona dentro do banco de dados, coisa completamente diferente das outras linguagens. Mas ela merece o prêmio por ultrapassar a marca de 800 linhas para fazer o que Python e F# fazem com 500.

Java, D e C++ ficaram com resultados muito mais parecidos do que eu imaginava. Algumas implementações têm algumas checagens a mais e outras a menos no código, mas fora isso não tem muita diferença.

Prêmio Imaturidade

D versão 2. Esbarrei em 2 bugs (um era que to!double("0") falhava e outro é que o executável com otimização não fechava os arquivos sozinho*, ao contrário do que a documentação afirmava). O bug do to!double foi corrigido quando atualizei da versão 2.048 para 2.049, mas o fechamento dos arquivos teve que continuar manual (ainda bem que D tem o recurso scope(exit), muito mais legal que finally. É na verdade genial). Nessas horas eu penso que deveria ter usado D 1, que é mais estável (o D 2 ainda recebia modificações radicais até recentemente, mas está começando a estabilizar). Não sei se D não foi injustiçado por eu ter usado a versão bleeding-edge, mas praticamente todas as [implementações de ] linguagens que eu usei são bem recentes.

*Depois descobri que eu estava criando sem querer cópias do objeto File, porque ele é uma struct e não uma class. Corrigindo isso o problema sumiu e o desempenho melhorou. Certas linguagens parecem ser menos amigáveis para iniciantes, como C++ e D…

Prêmio Mas eu tenho que fazer isso na mão?

1º lugar:

C++, por:
– ter que implementar uma função hash para fazer uma chave composta num unordered_map.
– por seus copy constructors, move constructors, fucking constructors, smart pointers, dumb pointers e references.

E para a STL do C++, por:
– ter que comparar com string::npos ou container.end() para saber se um elemento foi encontrado depois de uma busca;
– ser uma complicação aplicar a função hash personalizada ao unordered_map (struct com operator() const);
– find num map retornar um pair, sendo que eu só estou interessado no segundo elemento.

Sorte que por causa das novidades do C++1x eu não precisei usar um sstream (ou o inseguro sprintf) para converter números para strings…

2º lugar: Java, também por ter que implementar uma função hash para fazer uma chave composta num HashMap. Eu poderia ter concatenado os campos numa string e ter pegado o hash dessa string (como eu fiz em PL/SQL), mas resolvi fuçar na implementação do tipo Long e peguei a função hash de lá. Dei uma alterada pra fazer hash de 2 longs, não sei se prestou. Depois fiz algo parecido no C++.

Prêmio Viva, não preciso implementar funções hash!

Por conseguinte, pode-se dizer que Python, F# e D ganharam o prêmio “Viva, não preciso implementar funções hash!”

Prêmio Não tá faltando nada, não?

F#, por não precisar (nem permitir) usar return no final das funções.

— Pra retornar um 0 do main é só deixar o 0 solto ali no fim?

— É.

— Puxa!

Prêmio Union

F#. Definir o tipo PdfObject foi muito fácil em F#, disparado.

Prêmio Metaprogramação

D. Defini todas as subclasses de PdfObject usando geração de código a partir de strings. Demorou um pouco até eu conseguir fazer algo que não desse uma mensagem de erro bizarra, mas foi legal.

Ctrl+C, Ctrl+V é para fracos. Em D, o compilador faz isso por você.

Prêmio Diferente de todo o resto

F#. Eu já tinha lido um pouco sobre a família dessa linguagem (ML), mas foi a primeira vez que programei numa linguagem dessas. Refrescante, mesmo que eu tenha usado poucas abordagens novas. O legal é F# não tem a distinção de statement e expression (tudo é expressão). Longe de ser característica exclusiva de F#, mas acabou que de todas as linguagens que testei, ela foi a única com essa característica.

Prêmio {Curly braces}, que tédio

D, Java, C++. Depois de tanto programar com {chaves}, dá um certo tédio ver mais uma linguagem nesse estilo. Não que eu prefira estudar coisas bizarras como APL, Forth ou Lisp*, nem morto. Mas é legal de vez em quando escrever um then ou apenas dois-pontos depois da condição de um if.

*Trollando lispeiros só pra rir um pouco

Prêmio Menor linha de comando

Python. Não precisa passar nada de especial na linha de comando pra rodar. As otimizações das opções -O e -OO são o mesmo que nada, praticamente.

Menção especial ao Java também, por não precisar de nenhuma opção de otimização e ainda assim ter bom desempenho (sem contar a inicialização, claro).

Prêmio Pipes e foreach, viva!

PowerShell, claro. Facilitou muito os testes. Se não fosse o PowerShell, teria escrito em Python, seria interessante também.

Prêmio Pseudocódigo

Python, que é praticamente pseudocódigo que executa (junto com Lua). Linguagens sempre acabam tendo esquisitices, Python e Lua se destacam por não se renderem à criação de $sintaxes <| bizarras. D e Java também têm potencial de se parecerem com pseudocódigo (só que com {chaves}), mas não acertaram exatamente no alvo, falta ou sobra algo…

Prêmio REPL

Python, por permitir testar trechos de código num ambiente interativo (Read-Eval-Print Loop) e permitir listar membros, ver os tipos das variáveis e sua documentação.

F#, porque também tem um REPL (o F# Interactive) e ajudou muito. Mas para alguns detalhes era mais prático usar o IntelliSense do Visual Studio do que o F# Interactive.

Prêmio Complicação desgraçada fiadapu

C++. Vejamos um exemplo:

Em Python, uma estrutura do tipo hash table, se chama dict. Esse é o nome do tipo, e existe a sintaxe com {} para construir um diretamente (embora seja possível construir com dict também).

Em Java, um tipo equivalente seria:

HashMap<String, PdfObject>

É necessário declarar o tipo dos elementos e não há sintaxe especial para inicializar a estrutura, mas tudo bem.

Na verdade, o nome completo do tipo é

java.util.HashMap<java.lang.String, PdfObject>

Um pouco mais complicado, mas nada de mais.

Em C++, eu usei um

unordered_map<string, shared_ptr<PdfObject>>

É um pouquinho maior, e eu precisei usar um shared_ptr para não ter que me preocupar com a desalocação dos PdfObject. Não é tão ruim, né?

Bom, internamente esse tipo é traduzido para:

std::tr1::unordered_map<
  std::basic_string<
    char,
    std::char_traits<char>,
    std::allocator<char>
  >,
  std::tr1::shared_ptr<PdfObject>,
  std::hash<
    std::basic_string<
      char,
      std::char_traits<char>,
      std::allocator<char>
    >
  >,
  std::equal_to<
    std::basic_string<
      char,
      std::char_traits<char>,
      std::allocator<char>
    >
  >,
  std::allocator<
    std::pair<
      std::basic_string<
        char,
        std::char_traits<char>,
        std::allocator<char>
      > const,
      std::tr1::shared_ptr<PdfObject>
    >
  >
>

“Simples”, né? E é isso que aparece nas mensagens de erro e no IntelliSense, só que sem indentação!

Prêmio C++ está afundando na própria complexidade, vamos começar de novo e fazer certo

Esse prêmio vai para o D, obviamente. Pena que D (pelo menos a versão 2) também ganhou o prêmio de imaturidade…

Prêmio Máquina virtual? E se eu não quiser levar 100MiB junto com o meu programa?

C++ e D. Dispensas explicações. Queria colocar mais linguagens nessas categoria, o que eu incluiria?

Comparação de desempenho lendo PDFs

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.

Edit 2014-04-12: Todos os fontes no BitBucket (o repositório é Git):
https://bitbucket.org/marcuscf/pdfpagecount/commits/tag/v2010-09-19

Código fonte em Java

Código fonte em C++

Código fonte em D

Código fonte em F#

Código fonte em Python

Script em PowerShell pra rodar tudo isso.

Próximo post: premiação.