2007-04-24

Comparação de desempenho

Kodumaro
Estou estudando Haskell ao mesmo tempo em que me aperfeiçoo em Lua e Kepler.

Meu interesse em Haskell é usá-la como linguagem compilada, enquanto continuo usando Lua (e eventualmente Python) como linguagem geral.

Estava impressionado com a velocidade de Haskell em recursões, principalmente no cálculo de Fibonacci (que costumo usar para comparações de desempenho), resolvendo índices altíssimos em frações de segundo, enquanto a própria linguagem C demora para retornar algum resultado.

Foi então que me dei conta de que a implementação em Haskell de Fibonacci que eu estava usando aplicava memoização!

Resolvi refazer os códigos comparatórios usando memoização também nas outras linguagens.

Observação:


  1. Não refiz em Java porque não conheço a linguagem o suficiente para trabalhar com memoização (imagino que haja algum módulo pronto) e números grandes.
  2. Não refiz em C e C++ porque fiquei com preguiça de reescrever usando GNU MP.


Fiz os testes da seguinte forma: executei cinco vezes cada «programa» anotando seus tempos de execução; removi o maior e e o menor tempos; calculei a média geométrica.

Foi aí que tive uma surpresa agradável: Lua foi a segunda mais rápida!

Seguem as médias:
  • Haskell: 4,0ms
  • Lua: 21,7ms
  • [update 2007-05-02]Ruby: 42,0ms[/update]
  • Python: 58,0ms
  • Perl: 97,3ms


[update 2007-05-02]
Escrevi errado da primeira vez!

Havia colocado microssegundo (μs) em vez de milissegundo (ms). Minha máquina não é tão rápida assim. =P

Desculpem!
[/update]


Agora os códigos (retirei os comandos de saída para stdout para medir as velocidades):

Haskell


module Main where

import IO

fib = 1 : 1 : zipWith (+) fib (tail fib)

main = do
hSetBuffering stdin LineBuffering
let num = fib !! 1000
putStrLn (show num)


Lua


local fib
do
local memo = setmetatable(
{ ["0"] = 1, ["1"] = 1 },
{ __mode = "k" }
)

function fib(n)
if not memo["" .. n] then
memo["" .. n] = fib(n - 2) + fib(n - 1)
end
return memo["" .. n]
end
end

local num = fib(1000)
print(num)


[update 2007-05-02]
Ruby

def fib(n, memo={ 0 => 1, 1 => 1 })
if not memo.member? n
memo[n] = fib(n - 2, memo) + fib(n - 1, memo)
end
return memo[n]
end

num = fib(1000)
print(num)


Obrigado Fenrrir!
[/update]


Python


def fib(n, memo={ 0: 1, 1: 1 }):
if not n in memo:
memo[n] = fib(n - 2) + fib(n - 1)
return memo[n]

num = fib(1000)
print(num)


Perl


use Memoize;

sub fib {
my $n = shift;

if ($n < 2) {
return 1;
} else {
return fib($n - 2) + fib($n - 1);
}
}

memoize 'fib';

my $num = fib 1_000;
print "$num\n";

exit 0;



[]'s
Rodrigo Cacilhas

14 comentários:

Walter Cruz disse...

Eu mandei! mas ele dá erro de stack

La Batalema Pitonisto disse...

Ô Walter!

Como eu vou marcar o tempo que Ruby leva para levantar, calcular o 1001º elemento (fib 1000) e encerrar se ele não consegue calcular o 1001º?

=P

[]'s

Cláudio Torcato disse...

Blz de post, Cacilhas! Gosto dessas comparações entre linguagens, principalmente para ver a sintaxe delas ;)

fenrrir disse...

Cara achei o post muito interessante. Não sei ruby, só vi o básico. Mas com o algoritmo a baixo, muito similar ao seu de python ele funciona.

def fib(n, memo={ 0=> 1, 1=> 1 })
if not memo.member? n
memo[n] = fib(n - 2, memo) + fib(n - 1, memo)
end
return memo[n]
end

num = fib(1000)
print(num)

O detalhe para que ele funciona é passar memo na chamada de fib dentro do if. O que achei estranho de não precisar em python. Conheco um pouco de python também. Pelo que percebi se o memo não é passado ali dentro (no código em ruby) a memoização não funciona, e por isso ele não funciona (não do jeito esperado).

Agora eu fiquei com a seguinte dúvida, por que em python, a cada recursão o dicionário memo preserva seus dados ? Sem ser passado como parametro na chamada da função.

Eu imaginava que a cada recursão memo receberia o valor default {0:1, 1:1}. Será que você poderia me ajudar com essa?

La Batalema Pitonisto disse...

Olá Fenrrir!

Muito obrigado pelo código! Segunda-feira vou testar na mesma máquina em que testei os demais, aí poderei postar as comparações.

Admito que tive um pouco de dificuldade para entender sua dúvida…

Você quer saber porque o dicionário em Python preserva os dados. É isso?

O que acontece é que em Python o dicionário é criado na primeira chamada da função apenas e o mesmo objeto dicionário é usado nas demais chamadas (a menos que seja passado algum parâmetro que o sobreponha, claro).

É na verdade um exemplo implícito de closure, que garante certa persistência e isolamento de dados.

O código Lua traz um exemplo explícito de closure.

Consegui explicar?

[]'s

fenrrir disse...

Ola Rodrigo,

Eu imaginei que tivesse algo a ver com closure, só que não sabia que closures se aplicavam também a recursões, outra coisa que me fez parar pra pensar foi o fato de por exemplo o "memo" da segunda recursão de fib não receber o valor default já que memo não foi passado, e ele usar o do closure. E ainda mais o fato de em ruby isso não funcionar, pois sempre vejo muito texto que fala de closure abordar ruby.

fenrrir disse...

Completando...

Sim, conseguiu :D. Obrigado.

La Batalema Pitonisto disse...

Olá Fenrrir,

Nas chamadas de fib() na recursão (e também fora dela, seguintes à primeira) não é necessário passar o dicionário pois, não sendo informado o parâmetro, será usado o mesmo objeto (dicionário) criado na primeira chamada (ou seja: closure implícito).

O legal disso é que mesmo em outras chamadas externas ao corpo da função o objeto dicionário – e todos seus pares chave-valor – são preservados.

[]'s

Walter Cruz disse...

Eu fiz um código em Ruby mais parecido com o de Perl, mas na minha máquina morre no 895.

require "rubygems"
require "memoize"

include Memoize

def fib( num )
return num if num < 2
fib(num - 1) + fib(num - 2)
end

memoize :fib
puts fib(895)

fenrrir disse...

Ah não, até ruby se saiu melhor que python :S

La Batalema Pitonisto disse...

Fica assim não, Fenrrir!

Lembra que Perl foi 67% mais lento que Python. =)

E também não coloquei Boo

Uma vez instalei o Boo (que é uma linguagem interessante), mas, quando comparei o desempenho com Python, fiquei muito decepcionado e larguei Boo de lado.

[]'s
Rodrigo Cacilhas

Milano Carvalho disse...

Cara, por que indexar as tabelas em Lua com strings? Indexe com número mesmo! E não há necessidade de criar uma metatable pra especificar __mode = "k", isso só seria necessário se os índices fossem objetos. Pode ficar tranqüilo que nesse caso o coletor de lixo captura essa tabela.

local memo = {[0] = 1, [1] = 1}
local function fib(n)
if not memo[n] then
memo[n] = fib(n - 2) + fib(n - 1)
end
return memo[n]
end

La Batalema Pitonisto disse...

Olá Milano!

Há uma razão para usar metatabela e índices em string.

É para preservação de memória.

Dá uma olhada no capítulo 17 do PiL.

[]'s

Milano Carvalho disse...

Não tem necessidade! Isso seria necessário se você indexasse a tabela com um objeto como chave, por exemplo:

local t = {}
t[{}] = true

Aí sim seria necessário usar referências fracas! E veja que esse tipo de indexação não é muito comum.
Não tem nada a ver com índices numéricos em uma tabela.
Leia o PiL novamente...
Não tem pra que querer complicar.