terça-feira, 30 de março de 2010

Lies, damn lies and benchmarks

Esses dias me deparei com um texto que mostra um benchmark simplificado (bastante ingênuo, na verdade) cujo título é "Holy Shmoly, Ruby 1.9 smokes Python away!". O benchmark baseia-se na execução do cálculo da série de Fibonacci. A implementação é recursiva e exercita, basicamente, chamadas de função e aritmética básica. São comparadas as implementações de Python e Ruby.

Embora seja muito simplificado, este benchmark desencadeou várias surpresas.

Surpresa 1 -- A implementação de Ruby está ficando mais rápida



Esta surpresa, na realidade, foi dupla para mim. Eu tinha idéia de que a implementação de Ruby era "meio lenta", mas não sabia que era tão lenta. A parte boa da surpresa é que a versão 1.9 está consideravelmente mais rápida.

Como referência para as próximas medições, abaixo estão os tempos de execução que obtive na minha máquina com as implementações em Ruby para Fibonacci de 0 a 39 (Ruby 1.8 e 1.9):
def fib(n)
if n == 0 || n == 1
n
else
fib(n-1) + fib(n-2)
end
end

40.times do |i|
puts "n=#{i} => #{fib(i)}"
end
Ruby 1.8Ruby 1.9
real    9m26.377s
user 8m11.039s
sys 1m15.273s
real    1m14.576s
user 1m14.565s
sys 0m0.020s

Surpresa 2 -- O desempenho de Python



Ao longo deste texto vou mostrar algumas extensões que fiz ao benchmark original, o que acabou gerando mais surpresas. A primeira dela foi medir o desempenho de Python usando o JIT Psyco. Abaixo estão o código utilizado e os resultados obtidos na minha máquina:

Python puro (sem Psyco)


#! /usr/bin/env python
def fib(n):
if n == 0 or n == 1:
return n
else:
return fib(n-1) + fib(n-2)


for i in xrange(40):
print "n=%d => %d" % (i, fib(i))
time ./fib.py
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 2m21.908s
user 2m21.349s
sys 0m0.016s


Python com Psyco


#! /usr/bin/env python
import psyco
psyco.full()

def fib(n):
if n == 0 or n == 1:
return n
else:
return fib(n-1) + fib(n-2)


for i in xrange(40):
print "n=%d => %d" % (i, fib(i))
time ./fib.py
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 0m6.998s
user 0m6.948s
sys 0m0.008s

Com o uso de Psyco, o tempo de execução foi de ~2m22s para ~7s. Impressionante.


Surpresa 3 -- O desempenho de Chicken



Decidi estender o benchmark um pouco mais e implementar o algoritmo em Scheme, com Chicken:
(define (fib n)
(if (or (= n 0) (= n 1))
n
(+ (fib (- n 1)) (fib (- n 2)))))

(let loop ((n 0))
(when (< n 40)
(print "n=" n " => " (fib n))
(loop (+ n 1))))


O desempenho de Chicken, quando interpretando código, é violentamente lento (e esperado, pelo menos por mim):
time csi -s fib.scm
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 5m21.635s
user 5m20.696s
sys 0m0.916s

Quando o código é compilado com Chicken, há uma melhora significativa no desempenho:
$ csc fib.scm
time ./fib
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 0m29.996s
user 0m29.938s
sys 0m0.060s

Usando um pouco de agressividade na otimização:
$ csc -O5 fib.scm
time ./fib
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 0m21.506s
user 0m21.405s
sys 0m0.012s

Com a compilação do código em Chicken, foi possível baixar o tempo de execução de mais de 5 minutos para pouco mais de 20 segundos.

Por curiosidade, resolvi ver o tempo que uma implementação em C, nos mesmos moldes, levaria:
#include <stdio.h>

int fib(int n) {
if (n == 0 || n == 1) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}


int main() {
int n;
for (n=0; n<40; n++) {
printf("n=%d => %d\n", n, fib(n));
}
}
$ gcc -O3 -ffast-math -fomit-frame-pointer -o fib-c fib-c.c
$ time ./fib-c
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 0m1.374s
user 0m1.372s
sys 0m0.004s


Até aqui, temos C, Python + Psyco e Chicken (compilado) nas três primeiras colocações em tempo de execução. Porém, há um detalhe na implementação em Chicken que faz uma certa diferença: o operadores numéricos (e.g., +, =) aceitam um número arbitrário de argumentos, diferentemente dos operadores numéricos das outras linguagens, que são binários. Obviamente, há um custo para isso. Entretanto, Chicken dispõe de procedimentos binários para operações numéricas com inteiros (com prefixo fx):
(define (fib n)
(if (or (fx= n 0) (fx= n 1))
n
(fx+ (fib (fx- n 1)) (fib (fx- n 2)))))

(let loop ((n 0))
(when (fx< n 40)
(print "n=" n " => " (fib n))
(loop (fx+ n 1))))


compilando e executando este código, temos:
$ csc -O5 fibfx.scm
$ time ./fibfx
n=0 => 0
n=1 => 1
...
n=39 => 63245986

real 0m1.088s
user 0m1.088s
sys 0m0.000s

A surpresa aqui é que o tempo de execução de uma implementação em Scheme é menor do que o de uma implementação em C. E, mais intrigante ainda, é que Chicken compila para C. :-) Porém, a estratégia de compilação usada por Chicken gera código C mais otimizado do que um humano programando.

No final das contas, temos os seguintes tempos, ordenados por ordem decrescente de tempo de execução:
Ruby 1.89m26.377s
Chicken (interpretado)5m21.635s
Python puro2m21.908s
Ruby 1.91m14.576s
Python + Psyco0m6.998s
C0m1.374s
Chicken (compilado)0m1.088s


As seguintes versões de interpretadores e compiladores foram usadas:
Python2.6.4
Psyco1.6
Ruby1.8 e 1.9
Chicken4.4.4
GCC4.4.1

As medições foram feitas em um processador de 2.4GHz com sistema operacional Ubuntu Linux.

Vale ressaltar, novamente, que esse benchmark, embora tenha gerado algumas surpresas, representa muito pouco para avaliação de desempenho de implementações de linguagens de programação.

segunda-feira, 29 de março de 2010

Publicação dinâmica de arquivos estáticos com awful

Hoje um usuário no canal #chicken (freenode.net) levantou a questão de como servir conteúdo dinâmico a partir de arquivos estáticos, mas sem publicar os arquivos estáticos diretamente. É, parece confuso. Um exemplo seria ter o diretório de documentos do servidor vazio e gerar conteúdo dinâmico a partir de arquivos estáticos e código executado no servidor. Isto é exatamente o que awful faz (via define-page). Porém, o usuário queria disponibilizar conteúdo dinâmico em vários formatos, não apenas em [X]HTML. Por exemplo, servir uma imagem através da URL http://servidor/imagem, onde imagem não é um arquivo no diretório de documentos do servidor. Além disso, ele queria que http://servidor/imagem retornasse a própria imagem, não uma página em HTML com <img src="imagem">.

Confesso que isso não foi algo que cogitei quando implementei define-page. Pensei no uso de define-page para a definição de páginas em [X]HTML ou de texto, não para imagens, vídeos ou formatos arbitrários.

Para a minha surpresa, a solução é bastante simples com o uso do procedimento send-static-file, de Spiffy:
(define-page "img"

(lambda ()
(begin
(send-static-file "/home/mario/img.png")
"")))

O código acima faz com que send-static-file ajuste os cabeçalhos HTTP para indicar que a resposta é um arquivo de imagem (com base na extensão) e envie o conteúdo do arquivo dado como argumento.

Porém, send-static-file considera que o caminho do arquivo passado como argumento está a partir do diretório de documentos do servidor web (i.e., o caminho passado como argumento será concatenado com o diretório de documentos do servidor web). Tentativas de passar um caminho absoluto, obviamente, não funcionam, pois o caminho absoluto será concatenado ao diretório de documentos do servidor, possivelmente apontando para um arquivo que não existe ou, com certeza, apontando para o arquivo errado.

A solução para este problema, no entanto, também é fácil: basta temporariamente mudar o valor do parâmetro que indica o diretório de documentos do servidor (root-path):
(define-page "img"

(lambda ()
(begin
(parameterize ((root-path "/"))
(send-static-file "/home/mario/img.png"))
"")))

No exemplo acima, configurei o diretório de documentos do servidor web para ser /, de forma que eu possa referenciar arquivos através do caminho absoluto. O uso de parameterize garante que a definição temporária será válida apenas durante a execução do seu corpo (send-static-file, no exemplo).

Obviamente, a definição de URLs para envio de objetos estáticos de forma dinâmica pode ser simplificada se definirmos um procedimento para isso:
(define (define-static-file url-path abs-path)
(define-page url-path
(lambda ()
(begin

(parameterize ((root-path "/"))
(send-static-file abs-path))
""))))

Estando define-static-file definido, publicar um arquivo estático de forma dinâmica, basta:
(define-static-file "img" "/home/mario/img.png")

quarta-feira, 24 de março de 2010

Awful em 35 segundos

Hoje vi na lista de usuários brasileiros de Python uma mensagem em que era apontado um vídeo sobre o framework Bottle. O vídeo mostra o desenvolvimento de um "hello world" no framework em 36 segundos. Resolvi fazer o mesmo com awful e consegui (oh!) em 35 segundos.



Típica medição inútil, mas aí está. :-)

Não sei se é possível visualizar o vídeo em uma resolução maior com esse tocador de vídeos do blogger. Na dúvida, coloquei umas cópias em http://parenteses.org/mario/misc/awful.avi e http://parenteses.org/mario/misc/awful.ogv.

segunda-feira, 8 de março de 2010

Scheme sem parênteses

Para aqueles que não usam Scheme só por causa dos parênteses: seus problemas acabaram! Basta substituir os parênteses por colchetes.

[define [fatorial n]
[if [< n 2]
1
[* n [fatorial [- n 1]]]]]

[display [fatorial 5]]
[newline]


Exemplos com algumas implementações:

Chicken:
$ csi -s fatorial.scm 
120


Gambit:
$ gsi fatorial.scm 
120


PLT:
$ mzscheme -f fatorial.scm 
120


Ypsilon:
$ ypsilon fatorial.scm 
120


Embora [ e ] sejam caracteres reservados em R5RS, muitas implementações os utilizam como sinônimos para parênteses. É comum a prática de uso de colchetes para associações entre variáveis e valores em formas let. Exemplo:

(let ([a 3]
[b 4]
[c 5])
(+ a b c))


Eu, particularmente, não costumo usar colchetes como sinônimo de parênteses, mas já vi muito código que usa.

sexta-feira, 26 de fevereiro de 2010

try/catch em Scheme

Para quem não gosta da sintaxe para tratamento de exceções de handle-exceptions (SRFI-12), a seguir está uma pequena macro para transformar a sintaxe de handle-exceptions em uma com try/catch:

(define-syntax try
(syntax-rules (catch)
((_ attempt (catch exn handler ...))
(handle-exceptions exn
(begin handler ...)
attempt))))


Exemplos de uso:

(define (try-car l #!optional default)
(try (car l)
(catch exn default)))


csi> (try-car '(1 2 3 4))
1
csi> (try-car '())
#f
csi> (try-car '() '())
()

terça-feira, 23 de fevereiro de 2010

Arc challenge em awful

Há dois anos, Paul Graham propôs o seguinte desafio (arc challenge):

Write a program that causes the url said (e.g. http://localhost:port/said) to produce a page with an input field and a submit button. When the submit button is pressed, that should produce a second page with a single link saying "click here." When that is clicked it should lead to a third page that says "you said: ..." where ... is whatever the user typed in the original input field. The third page must only show what the user actually typed. I.e. the value entered in the input field must not be passed in the url, or it would be possible to change the behavior of the final page by editing the url.


Abaixo está uma implementação para o arc challenge em awful:

(use awful html-utils spiffy-request-vars)

(define-session-page "said"
(lambda ()
(with-request-vars $ (said)
(cond (said
($session-set! 'said said)
(link "said" "click here"))
(($session 'said)
=> (lambda (said)
(++ "You said: " said)))
(else (form (++ (text-input 'said)
(submit-input))
action: "said"
method: 'post))))))


Para testar o programa, basta executar:

$ awful arc-challenge.scm


E acessar http://localhost:8080/said

terça-feira, 16 de fevereiro de 2010

doctests em Scheme

No ano passado, o Luciano Ramalho criou um grupo para estudo do livro Structure and Interpretation of Computer Programs. O grupo ainda existe mas está com as atividades paradas atualmente. Boa parte dos componentes do grupo são usuários de Python. O Luciano é um usuário de Python e mencionou a falta, em Scheme, de uma facilidade que existe em Python: a possibilidade de de especificar testes em docstrings.

A minha argumentação com relação a esta funcionalidade em Scheme é de que não deve ser feita da mesma forma como em Python, pois em Scheme, diferentemente de Python, docstrings são ambíguas. O equivalente a docstrings em Scheme só poderia ser feito através de comentários (doccomments?).

Em Python, a ambigüidade não existe porque funções exigem um comando return para indicar que a função terminará e desviará o fluxo de execução para o ponto imediatamente seguinte ao de onde foi invocada, opcionalmente, devolvendo resultados. Em Scheme, não há um comando return explícito (a menos que seja definido um procedimento de escape de uma continuação, mas este é um caso bem específico e não muito usual). O valor produzido por um procedimento é o valor resultante da última expressão avaliada. No caso de procedimentos em que a única expressão a ser avaliada é uma string, tem-se a ambigüidade. Exemplo:

(define (proc)
"Isso é uma docstring ou uma string legítima?")


A ambigüidade que ocorre com docstrings é a mesma que ocorre com doctests. Se a única expressão a ser avaliada por um procedimento for uma string, como saber a string deve ser o resultado da avaliação do procedimento ou simplesmente uma docstring? Em Scheme, é uma ambigüidade. Em Python a ambigüidade é resolvida com o comando return.

Ignorando a ambigüidade que pode ocorrer em algumas definições, implementei em Scheme um esquema semelhante a doctests em Python. A implementação não é de uso geral, mas serve como " prova de conceito" (ainda que o conceito não esteja totalmente correto :-)).

A sintaxe das strings de teste é a seguinte:

  • a expressão sob teste, que seria digitada no REPL, deve ser precedida por >

  • o resultado esperado deve ser precedido por :



O parser das strings de teste é bastante limitado. Não são admitidas múltiplas linhas para expressões de teste nem de resultado. O parser ignora linhas não iniciadas por > ou :.

A única forma sintática para declaração de procedimentos admitida é:

(define (proc args)
body)


Outras formas, como as abaixo, não são suportadas:

(define proc
(lambda (args)
body))

(define proc #f)
(set! proc (lambda (args) body))

(define proc
(let ()
(lambda (args)
body)))


Teste de procedimentos resultantes da expansão de macros também não é suportado.

A implementação consiste, basicamente, de um procedimento doctest que lê todas as formas (forms) do arquivo em que é invocado e procura por definições com o padrão

(define (proc args)
"doctest"
body)


Então a string com os testes é extraída da definição e é passada para o parser de strings de teste, o qual avalia as expressões e os resultados esperados e imprime o resultado.

Um aspecto interessante da implementação é a forma como o parsing do código é feito: com manipulação de listas (estrutura de dados usada para representar código em Scheme).

O código da implementação, em Chicken Scheme, está abaixo:

(use posix (srfi 1 13))

(define (doctest)

(define (pick-teststrings forms)
;; Retorna uma alist '((procname1 . test-string1) (proname2 . test-string2) ...)
(filter-map
(lambda (form)
(and (list? form)
(eq? (car form) 'define)
(not (null? (cddr form))) ;; (define sym)
(and (pair? (cadr form)) ;; (define (proc ...))
(string? (caddr form))
(cons (caadr form) ;; procname
(caddr form))))) ;; test-string
forms))

(define (check test result)
(let* ((error-test #f)
(error-result #f)
(err-msg (lambda (e)
(with-output-to-string (cut print-error-message e))))
(pass
(equal? (handle-exceptions e
(begin
(set! error-test (err-msg e))
#f)
(eval test))
(handle-exceptions e
(begin
(set! error-result (err-msg e))
#t)
(eval result)))))
(if (or error-test error-result)
(display
(string-append
"Erro executando "
(->string (if error-test test result))
" --> " (or error-test error-result)))
(if pass
(print test " = " result " [ok]")
(print test " != " result " [fail]")
))))

(define (parse-teststring teststring)
(define (parse-line line prefix)
(and (string-prefix? (->string prefix) line)
(with-input-from-string
(string-trim line (lambda (c) (char=? c prefix)))
read)))
(let ((tests '())
(results '()))
(for-each (lambda (line)
(set! line (string-trim-both line))
(cond ((parse-line line #\>)
=> (lambda (expr)
(set! tests (cons expr tests))))
((parse-line line #\:)
=> (lambda (expr)
(set! results (cons expr results))))))
(with-input-from-string teststring read-lines))
(values tests results)))

(let ((forms (with-input-from-file (program-name) read-file)))
(for-each (lambda (procname/teststring)
(let ((procname (car procname/teststring))
(teststring (cdr procname/teststring)))
(print "===== " procname " =====")
(let-values (((tests results) (parse-teststring teststring)))
(for-each (lambda (test result)
(check test result))
(reverse tests)
(reverse results)))
(print "")))
(pick-teststrings forms))))


Abaixo estão exemplos de uso de doctests e, em seguinda, a saída da execução dos testes:

#!/usr/bin/csi -script

(load "doctest.scm")

(define (plus a b)
" Aqui estao os testes
> (plus 3 4)
: 7
> (plus 4 5 0)
: 8
> (plus 4 5)
: 8
"

(+ a b))


(define (minus a b)
"
> (minus (plus 3 4) 3)
: 4
> (minus 4 5 0)
: 8
> (minus 4 5)
: 8
"

(- a b))

(define (sort-list-string l)
"
> (sort-list-string '(\"b\" \"o\" \"p\" \"h\" \"v\"))
: '(\"b\" \"h\" \"o\" \"p\" \"v\")
"

(sort l (cut string-ci<? <> <>)))

(define (fatorial n)
"
> (fatorial 1)
: 1
> (fatorial 2)
: 2
> (fatorial 3)
: 6
> (fatorial 5)
: 120
> (fatorial 10)
: 3
> (plus (fatorial 0) (fatorial 3))
: 7
"

(if (< n 2)
1
(* n (fatorial (sub1 n)))))

(doctest)


A saída da execução dos doctests é mostrada abaixo:

===== plus =====
(plus 3 4) = 7 [ok]
Erro executando (plus 4 5 0) --> Error: bad argument count - received 3 but expected 2: #
(plus 4 5) != 8 [fail]

===== minus =====
(minus (plus 3 4) 3) = 4 [ok]
Erro executando (minus 4 5 0) --> Error: bad argument count - received 3 but expected 2: #
(minus 4 5) != 8 [fail]

===== sort-list-string =====
(sort-list-string (quote (b o p h v))) = (quote (b h o p v)) [ok]

===== fatorial =====
(fatorial 1) = 1 [ok]
(fatorial 2) = 2 [ok]
(fatorial 3) = 6 [ok]
(fatorial 5) = 120 [ok]
(fatorial 10) != 3 [fail]
(plus (fatorial 0) (fatorial 3)) = 7 [ok]