Em artigosanteriores eu descrevi algum dos problemas associados com herança, ou subtipagem, e questões ligadas a co/contravariância. Porém outra questão me chamou muito a atenção recentemente, é o fato de classes e objetos como normalmente vemos em linguagens com tipagem explícita são uma mistura de conceitos que talvez não deveriam estar juntos.
O que exatamente objetos de linguagens comuns, feito C++, oferecem? Basicamente três coisas: layout, interfaces e subtipagem. Uma classe derivada define uma estrutura de dados que é uma extensão do layout daquelas de seus parentes; o mecanismo de funções virtuais é a única forma sã de se definir interfaces; e, por fim, herança é a forma de se definir uma relação de subtipagem entre dois tipos, no caso do C++ classes.
As duas primeiras propriedades são realmente úteis, a terceira é questionável - mas não vamos atentar para isso agora. Não existe uma razão óbvia para deixar extensão estrutural e interfaces atadas uma a outra. De fato, é uma enorme limitação. Java e C# possuem o mecanismo de interfaces para resolver isso, porém ambas exigem que todas sejam definidas de antemão, ou seja, são uma minúscula fração do número potencial de interfaces que suportam. Ou seja, adora um modelo de interfaces prescritivo e não latente.
Algumas linguagens funcionais resolvem este problema adotando alguma forma de tipos de alta ordem*, que permitem uma função ser definida em termos da interface exigida de um objeto e basta este atendê-la para poder ser usado, independente de herança. Pode ser visto, a grosso modo, como um mecanismo equivalente à interfaces no Java ou C# onde cada classe não precisa definir quais suportam, basta implementar os métodos relacionados.
À primeira vista, tipos de alta ordem, ou classes de tipos na terminologia e implementação do Hakell, podem parecer a mesma coisa que templates do C++, porém a semelhança fica apenas na superfície, templates são um mecanismo de macro-expansão estruturada, não existe como validar uma função em sua definição ou uso, é necessário sempre fazer sua expansão para verificar sua validade. Outra diferença gritante é a ampla possibilidade de compartilhar código entre as várias instanciações de uma função que usa tipos de alta ordem. Existe uma longa literatura sobre o assunto e várias formas de favorecer uso de memória ou performance.
Mas voltando a questão de se subtipagem é uma propriedade interessante ou não para uma linguagem ter. Bom, teoricamente é muito interessante poder definir relações de substituibilidade, mas na prática, a maioria dos frameworks não se valem de tal princípio. Um bom exemplo é o pacote de coleções do Java, ela é toda construída em termos de interfaces e herança é usada como uma forma tosca de promover reuso de código.
O súbito interesse por linguagens funcionais nesses últimos anos vem trazendo a tona uma série de avanços que estas linguagens possuem e estavam até então restritos à comunidade científica. Está mais que na hora para começarmos a repensar todos os antigo erros e quimeras que as linguagens de programação de mercado promovem e começar a pensar como produzir algo que nos permita dar o mesmo salto dos anos 60 que Algol e Fortran garantiram - e dessa vez nenhum idéia pode ser refém.
*A wikipedia, apesar de extensão não tem uma referencia para tipos de alta ordem, então vou me valer como referencia lógica de alta ordem e deixar como exercício a aplicação do mesmo para teoria de tipos. Type classes do Haskell podem ser construídas aplicando lógica de alta ordem à um sistema de tipos básico como o descrito em “Basic Simple Type Theory - Hindley, J. Roger”.
Um pais pode aceitar um terrorista convicta como ministro? E que tal um assaltante de bancos? Por que não os dois? Afinal temos um descontrolado que entregou a casa civil à Dilma Rousseff.
O Presidente Inácio anunciou que ela é sua favorita para 2010. A noticia é boa em um aspecto, significa ainda mais o fim da Marta - e que esta um dia explique seu envolvimento com ONGs durante sua prefeitura.
Porém como eu posso não ficar apavorado com a idéia de ter uma mulher que ficou nacionalmente conhecida pelos escândalos do dossiê dos gastos do FHC e da venda da Varig?
Vamos realmente querer essa mulher, que acredita em sacrificar inocentes* para atingir seus objetivos, como presidente? A hora de agir é essa, não podemos permitir que o Bêbado da Silva use a máquina estatal para fazer a campanha de uma assaltante.
* Ou alguém por acaso acha que o Banespa estava vazio em 1968?
Como podemos produzir uma representação visual da evolução de um projeto? O pessoal do projeto code swarm tem uma ótima resposta. Um vídeo gerado a partir do histórico de commits do projeto.
O mais legal disso tudo é que o software é de código aberto, então qualquer um pode produzir vídeos também. Resolvi então fazer uma série deles baseados no mono. Abaixo está o primeiro deles, mostrando o moonlight. Fico devendo o áudio para o próximo.
Interpretadores voltaram a ser um assunto muito discutido devido aos resultados alcansados pela SquirrelFish (interpretador de javascript do Webkit) e ao fato da VM do Android também usar um. Curiosamente, ambas VMs são baseadas em registradores em vez de pilha, como as máquinas virtuais tradicionais como JVM e CLR.
A grande diferença está na forma como valores intermediários são calculados. Em uma VM baseada em pilha, operandos são adicionados ao topo da pilha enquanto operadores os retiram e realizam alguma operação. Uma máquina baseada em registradores usa de variaveis para armazenar resultados intermediários.
Para exemplificar melhor, o código “a = b * c + 10″ ficaria algo como:
Máquina de pilha:
push_local 'b'
push_local 'c'
mul
push_const 10
add
store_local 'a'
A primeira vista a diferença é quase que estética, porém se considerarmos quanto espaço precisamos para representar o código e o número de operações de leitura e escrita à memória cada uma faz, vamos notar como são técnicas fundamentalmente diferentes.
Vamos assumir um formato bem simples para representar o código de ambas, que é um byte para designar a operação seguido de um byte para cada operando. Aplicando esta forma, temos.
Fica claro aqui que uma máquina de pilha admite uma representação muito mais compacta do mesmo programa. Em geral essa relação não é tão gritante, pois existe uma série de formas de diminuir o número de instruções necessárias para uma máquina de registradores.
Agora vamos analisar quantas operações de memória cada máquina faz, verificando cada instrução individualmente.
Maquina de pilha:
push_local X
lê a variavel local X
lê o endereço atual do topo da pilha
escreve X no topo da pilha
incrementa e armazena o novo valor para topo da pilha
mul / add
lê o endereço atual do topo da pilha
lê o valor do topo da pilha
lê o valor abaixo do topo da pilha
escreve o resultado no local abaixo do topo da pilha
decrementa e armazena o novo valor para topo da pilha
push_const X
lê o endereço atual do topo da pilha
escreve X no topo da pilha
incrementa e armazena o novo valor para topo da pilha
store_local X
lê o endereço atual do topo da pilha
lê o valor no topo da pilha
escreve o valor em X
decrementa e armazena o novo valor para topo da pilha
Aplicando esses valores ao programa em questão temos um total de 25 operações.
Máquina de registradores:
load_local X <= Y
lê o valor da variavel local Y
escreve o valor no registrador X
mul / add X <= Y, Z
lê o valor do registrador local Y
lê o valor do registrador local Z
escreve o valor no registrador X
load_const X <= Y
escreve o valor de Y no registrador X
store_local X <= Y
lê o valor do registrador Y
escreve o valor no registrador X
Fazendo a conta, temos um total de 13 operações.
Ironicamente neste caso, uma máquina de registradores leva uma significante vantagem em relação a uma máquina de pilha. Apesar de se tratar de uma simplificação, na prática o resultado é o mesmo. Em máquinas modernas onde o custo de acessar a memória principal é muito grande, uma máquina de registradores tem o potencial de ser mais rápida.
Essas dicotomia entre qual admite uma representação mais compacta e qual permite uma implementação mais eficiente é fundamental na arquitetura de qualquer máquina virtual. Apesar disso, existe muito além da simples forma de funcionar do interpretador que define a real performance da VM, ainda mais se falarmos de linguagens dinâmicas. Em um artigo futuro pretendo mostrar uma implementação de exemplo de ambos os tipos de máquinas virtuais.
Otimização não é difícil apenas pelo fato de que tornar um pedaço de código mais rápido seja uma tarefa complicada, mas também por nem sempre ser possível ter oque medir ou mesmo saber como medir.
Nessas duas últimas semanas passei por isso, estou trabalhando em um novo recurso do JIT do mono e tinha que medir qual era o ganho de performance possível. Meu primeiro problema foi encontrar um benchmark que representasse uso real e que fosse significativo o suficiente para os números não ficarem perdidos no meio de muito barulho.
Uma vez que encontrei qual era um bom programa para medir a performance, me deparei com outro problema, o tamanho do working set. CPUs modernas trabalham com até três níveis de caching antes de ir à memória principal. O custo de um acesso a memória pode chegar a uma centena de ciclos, o suficiente para calcular o produto de uma matriz 4×4 por um vetor. Logo não importa o quao significante for o benchmark, se o working set dele não for realístico, o teste é inválido pois tudo executara do cache, bem diferente no mundo real.
Ou seja, é necessário dimensionar e modelar o teste para cada ciclo não utilizar dados que sobraram em cache da execução anterior. Para isso é preciso conhecer o tamanho e a hierarquia de cache do seu processador, mesmo porque isso tem um impacto muito significativo na forma que o código deve ter.
Isso leva ao último desafio que tive de enfrentar, indeterminismo nos resultados do benchmark, a variância dos resultados era grande demais para ser apenas interferência externa, ela chegava a 30% para execuções de de algumas dúzias de segundos. A única diferença eram os endereços de alguns arrays usados extensivamente. Mais precisamente, a discrepância era no alinhamento dos elementos. Nos testes lentos eram múltiplos de 4, nos rápidos eram de 16. Novamente uma peculiaridade dos processadores modernos, que preferem os dados alinhados em endereços múltiplos de 8 ou 16.
Pode até parecer piada, mas o mesmo teste, depois de ajustado todos os detalhes aqui descritos, saiu de um ganho de 10% para 300%. Diferença essa que não é só resultado de um benchmark, mas aplicável ao uso real que esperamos que veja a tenha. Recurso esse que espero integrar ao mono muito em breve.
Hoje fiquei sabendo através de um amigo que o sistema de chamados da anatel possui um furo de segurança que expõe todos dados dos chamados feitos por qualquer pessoa. Incluindo usuário e senha. Sim, isso mesmo, usuário e senha. A vulnerabilidade é tão grande que permite coletar os dados de todos usuários do sistema de maneira trivial por qualquer pessoa que entenda o básico de como funciona a web.
Os irresponsáveis pelo sistema permitem consultar os dados de um chamado sem qualquer forma de autenticação. Basta acessar essa url e trocar o ZZZ pelo número do chamado. Fosse apenas esse o problema não estaria eu tão assustado, porém se olharmos com cuidado para o problema vamos notar uma série de agravantes.
A página inclui uma enorme quantidade de dados pessoais do usuário, desde endereço a telefone e CPF. Estes são dados mais que suficientes para realizar vários tipos de fraudes. Fosse apenas este o problema não estaria tão assustado, pois seria necessário saber o código da solicitação para ler tais informações.
Vamos então verificar a segurança do código do chamado. Abra dois chamados, um em seguida do outro, e note que se trata de um número seqüencial pequeno sem qualquer forma de verificação ou randomização. Ou seja, é trivial coletar todos chamados com ferramentas simples disponível em qualquer computador pessoal.
Finalmente vem a cartada final, reparem nos dados exibidos na página, eles incluem email e cpf. Até ai tudo bem, até você ir para a página de login do sistema e descobrir que para entrar você deve informar, acredite, seu email e cpf. Ou seja, temos como fazer não só harvesting de dados dos usuários, mas de suas senhas também.
Segurança como essa é inadmissivel para um órgão governamental. Isso é ridículo, é um afronte a nossa privacidade. Os amadores que fizeram esse sistema ignoraram todas regras básicas de segurança que qualquer desenvolvedor safo tem a obrigação de saber. Anatel, corrija isso com urgência e tome as devidas medidas administrativas para esse tipo de desastre não ocorra novamente. Por favor, a todos que lerem este texto, liguem já para a Anatel no 0800 33 2001, registrem uma reclamação formal e divulguem esse problema para o quanto antes ser solucionado.
Performance nunca foi um assunto fácil. Medir é complicado, comparar resultados é quase irrelevante, comparar linguagens é inútil. Porém as pessoas continuam insistindo no assunto, de um lado os que defendem linguagens gerenciadas dizendo que elas são tão rápidas quanto C ou C++; do outro lado ficam a demonstrar como isso não é possível.
Não se trata apenas do uso de micro-benchmarks, ou de otimizar melhor um alvo, ou da metodologia ser falha. Se trata apenas de falta de comprometimento. Não se pode medir performance daquilo que não se está envolvido pois o resultado é irrelevante. Qualquer tentantiva nessa direção só vai provar que a pessoa é tola.
Quando falo em compromisso me refiro ao fato de que não adianta tentar medir a performance sem estar envolvido na melhora dela. Medições devem ser feitas em softwares reais, não de fabricações de Oz. E o corolário disso é que comprar linguagens é um exercício de futilidade, pois não faz sentido manter o mesmo programa em Java e C++, por exemplo.
A performance de um programa é fator de muito mais coisas que apenas a velocidade bruta do meio de execução - seja uma VM ou código nativo. Principalmente para grandes sistemas, a complexidade arquitetural e características sistêmicas tem muito maior influencia. Já vi isso acontecer demais para ignorar.
Isso significa que micro-benchmarks são irrelevantes e pura obra do ego de seus criadores? Não, pelo contrário, diz apenas que quase sempre as pessoas envolvidas não estão comprometidas com os resultados. Um exemplo prático, medir a performance com ponto flutuante do gcc só é útil aos seus desenvolvedores, pois somente estes tem o compromisso em melhorá-la, para os demais é uma enorme perda de tempo.
Toda discussão de “Minha linguagem é mais rápida que a sua” parece esquecer de que para aplicações de significativa complexidade a maioria dos micro-benchmaks usados são irrelevantes. Para a maioria dos projetos, oque importa é quanto a plataforma torna fácil escrever programas que sejam rápidos. Não adianta ser super veloz se o desenvolvedor vai precisar fazer uma enorme série de sacrifícios para manter sua produtividade.
Existe uma série de exemplos bem intrigantes de como escolhas óbvias trazem resultados não óbvios em termos de performance. Desde gerenciamento de memória a execução especializada de código. Porém não espero ver as pessoas se eduquem nesse ponto, pois é uma causa fundamentalmente perdida, já que não existe razão suficiente contra um ego ferido e o fato de alguém na internet estar errada.
Essa foi uma pergunta que eu me fiz outro dia. Ela surgiu durante uma discussão sobre possíveis soluções para o inferno diário de milhões de pessoas. O argumento da maioria dos presentes era que a única real solução era construir metro. Vim a acreditar que essa é, de longe, aquilo que nossa cidade precisa.
Basta um pouco de matemática para provar que não é cavando túneis que vamos reduzir o caos do transporte. Vamos pegar o caso de uma cidade com um transporte considerado referencia global. Londres possui 408km de metro para 7,5 milhões de habitantes, ou um quilômetro para cada 18,3mil pessoas. Enquanto isso na região metropolitana de São Paulo sobrevivem 19,2 milhões de pessoa com 68km de metro e 257km de trem, ou seja, 282,3mil se debatendo por quilômetro de metro ou 59mil se espremendo no transporte metroviário da cidade.
Fazendo uma simples regra de três, a cidade precisa de mais 720km de trilhos para ter o mesmo nível de conforto de Londres. Pronto, resolvido, só construir agora. Porém as pessoas esquecem que dois detalhes, custo e prazos. Primeiro aos custos, a linha 4 foi orçada originalmente em 1,25 bilhões de dólares e tem uma extensão de quase 13km, o que significa 155 milhões de reais por quilômetro. Por esse parâmetro, São Paulo precisa de aproximadamente 112 bilhões em investimentos, ou sete vezes mais que os 15,8 destinados a todos programas do PAC para o ano de 2008. Mas vamos esquecer isso, pois dinheiro não é o problema. Sério.
O ponto importante é o tempo de construção. No atual ritmo de quatro quilômetros ao ano as obras ficariam prontas somente em 2188. Mas as coisas vão melhorar e vamos andar na mesma velocidade frenética da construção do metrô chines em Pequim, que é cinco vezes mais rápida. Ainda assim, as obras seriam entregues em 2044, daqui 36 anos.
Ouviram bem, serão trinta e seis anos de espera se, por milagre, passarmos a construir na velocidade maluca dos chineses. Vejamos, daqui todo esse tempo eu vou ter 62 anos e a todos dos leitores desse blog terão bem mais de cinqüenta. Ou seja, vamos estar na terceira idade, próximo de nos aposentarmos. Porém se o ritmo das obras se mantiver, talvez nossos bisnetos assistam a conclusão perto do final de suas vidas.
Eu não quero esperar até minha aposentadoria para ter um transporte urbano que preste na minha cidade. Não podemos esperar mais de três décadas, precisamos disso o mais rápido possível. Precisamos para ontem, não podemos que cada um jogo fora centenas de horas parado em engarrafamentos todo ano.
Dito isso, o quanto a tecnologia mudou nossas vidas nos últimos dez anos? Em especial, vamos refletir na questão organizacional das nossas vidas. Hoje é muito fácil via celular e internet organizar reuniões e grupos - basta um tweet “vamos beber” que em uma horas estão todos no bar certo. Hoje é muito fácil centralizar, catalogar, manipular e consultar informação pertinente a milhões de pessoas em tempo real - basta pensar em coisas como redes sociais, rss e sites de busca. Por fim, hoje a informação possui uma tremenda mobilidade, um celular moderno tem localização via GPS e até 1mb de largura de banda com a internet.
Agora vamos pensar em como opera o transporte público hoje. Temos linhas de ônibus fixas e definidas por engenheiros de tráfego baseados em pesquisar de origem e destino. Nós temos rotas estáticas de transporte, você conseguiria imaginar a internet sem roteamento dinâmico de pacotes? Nós temos rotas otimizadas segundo critérios antigos - o que seria do Google se a busca fosse baseada no conteúdo de um anos atrás apenas? Nós temos itinerários e linhas que não são adaptadas as necessidades dos usuários no momento que usam - imagine usar um navegador com 30 minutos de atraso.
Nosso sistema de transporte não deve ser modernizado, precisa ser recriado, repensado e quebrar todas barreiras convencionais. Porque você precisa pegar um ônibus que passa em lugares e horários pré-definidos? Imagine se bastasse do seu celular informar seu destino que um sistema ajustaria a rota de um veículo para melhor servir você e otimizar o uso das nossas vias? Algo quase como um taxi coletivo a custos de um convencional. Hoje temos todas as peças do quebra cabeça tecnológico para viabilizar isso.
Basta começar a pensar no assunto para centenas de idéias surgirem, a maioria delas muito melhor que as minhas. Basta pensar em um modelo de transporte que seja tão ágil quanto nossa vida moderna é, ou deveria ser.
Minha contribuição, entretanto, não é a sugestão tecnológica. O transporte de São Paulo é um mercado de 8 bilhões de reais ao ano, pelo menos. Muita grana não? Porém existe uma reserva de mercado para novas empresas entrarem e na forma de atuação delas, além de um cartel de empresas controlando o serviço hoje. Todos sabemos dos prejuízos que tais reservas causam ao interesse público, por isso a única solução é abrir o setor ao mercado como um todo e permitir que as empresas decidam como operar modal que irão oferecer. O resto deixem por conta da iniciativa privada, que é mister sua capacidade de inovação em face a livre concorrência.
Todo paulistano tem a obrigação de aproveitar que estamos em ano eleitoral, discutir o assunto, questionar seu candidato e tomar uma decisão informada em novembro. Não vamos mais uma vez deixar políticos nos enganarem com suas promessas vazias. Vamos dessa vez mostrar que não temos sangue de barata e mostrar exatamente aquilo que queremos. Eu estou aqui fazendo a minha, agora você Paulistano vá fazer a sua!
Sempre que leio qualquer coisa feita pelo Iam Piumarta fico impressionado. Outro dia ao ler os slides sobre o progresso do grupo dele para construir uma linguagem que seja uma real evolução ao que temos hoje, me dei conta de como aquilo que eles propõem é radicalmente diferente ao que a prática de que estamos habituados.
A atenção excessiva na meta-recursividade do sistema, assim como ser trivial criar e integrar novas gramáticas a linguagem, me faz pensar qual seria o limite de duas propriedade com emergence como essas. O efeito imediato é no compilador, que tem suporte direto a PEGs e pattern matching.
O compilador é capaz de se adaptar a mudanças nas regras de sintaxe e passar a usá-las instantaneamente apos serem definidas, ou seja, você pode definir uma mini linguagem em 5-10 linhas e usá-la logo em seguida. Não só isso, mas elas possuem escopo e podem operar em apenas um bloco de texto do seu fonte. Junta isso ao fato de que PEGs poderem ser compostas facilmente e o compilador suportar staged compilation que o resultado é ter uma linguagem que é maleável ao extremo.
A outra é a máquina virtual, que é construída com essa nova linguagem, de forma que ela mesmo siga o modelo de protótipos e delegação na sua implementação. Isso permite que ela seja tão flexível e dinâmica quanto suas aplicações. Essa é, de longe, a característica mais interessante, pois nenhuma linguagem baseada e máquina virtual que eu conheço chega a esse ponto.
Mas quais seriam aplicações reais disso? Aquilo que hoje fazemos com frameworks e bibliotecas, poderíamos fazer com DSLs. Então em vez de aprender um enorme framework que precisa seguir às regras da linguagem, bastaria aprender uma pequena nova linguagem. Da mesma forma, muita coisa que hoje se traduz em referencia direta ao código de bibliotecas, poderia ser feito como uma série de modificações à gramática corrente.
Porém eu acho que é possível ir muito mais além, com uma VM flexível, seria possível ajustar o modelo de execução para melhor realizar a tarefa que o programa deseja. Como, por exemplo, introduzir processos leves ao estilo do Erlang simplesmente incluindo uma biblioteca.
Quem ainda não se ligou o que gente como Ian Piumarta e Alan Kay tem feito no Viewpoints Research Institute deve parar um pouco e se dedicar àquilo que um dia poderia ser o próximo passo na evolução da computação.
Muito se fala em como as implementações de Ruby estão ficando rápidas, que estão evoluindo rapidamente. Porém não consigo pensar em como todas elas parecem mas preocupadas em repetir o caminho das pedras que outras linguagens dinâmicas passaram em décadas passadas.
Hoje a maioria ainda está no estágio de possuir um interpretador razoável e estar começando a investir em algum mecanismo primitivo de compilação para código nativo. Talvez a única diferença seja o uso de inline caches para resolução de nomes, que foi uma contribuição significativa do pessoal do Self no começo dos anos 90. Porém os principais resultados obtivos por pesquisas a um bom tempo, como tracing, especialização e especulação não deram as caras ainda.
Inline caching é uma optimização no qual o resultado da resolução de nomes, para dispatch de método por exemplo, é armazenado entre ativações distintas. O truque é introduzir uma clausula de guarda que verifica o tipo do objeto seletor e sua versão. Caches devem ser invalidados sempre que alguém alterar uma classe. Temos dois tipos de cache, os monomórficos que fazem caching de apenas uma invocação e os polimórficos, que armazenam uma série de resultados. Para melhor vamos exemplificar com pseudo código:
//ruby
def test (a)
a.foo
end
//pseudo-código gerado em C# para um cache monomórfico
class CallSite {
Type type;
MethodInfo method;
int version;
}
static CallSite test_site_0;
object test(object a) {
//guarda do cache monomórfico verifica tipo e versão
if (test_site_0.type == a.Type && test_site_0.version == A.Type.version)
return method (a);
else
//ResolveAndInvoke resolve o método "foo", invoca ele e atualiza o cache
return ResolveAndInvoke (a, "foo", ref test_site_0);
}
//pseudo-código gerado em C# para um cache polimórfico
delegate object InlineCacheNoArg (object this_);
static InlineCacheNoArg test_site_0;
object test(object a) {
return test_site_0 (a);
}
//pseudo-código inicial do delegate de test_site_0
object test_site_0_v0 (object _this) {
return ResolveAndInvoke (a, "foo", ref test_site_0);
}
//pseudo-código do delegate depois de chamarmos test (99)
object test_site_0_v1 (object _this) {
if (_this.Type == typeof (int) && _this.version == 1)
//o dispatch aqui pode ser via um delegate dependendo da resolução
return ((int)_this).foo ();
return ResolveAndInvoke (a, "foo", ref test_site_0);
}
//pseudo-código do delegate depois de chamarmos test ("str")
object test_site_0_v2 (object _this) {
if (_this.Type == typeof (int) && _this.version == 1)
return ((int)_this).foo ();
if (_this.Type == typeof (string) && _this.version == 1)
return ((string)_this).foo ();
return ResolveAndInvoke (a, "foo", ref test_site_0);
}
Como fica claro pelo exemplo, um cache polimórfico tem uma performance superior pois usa literais e funciona muito melhor no caso de não existir um tipo dominante entre as ativações. Aqui fica clara a limitação de uma das implementações, o JRuby não pode se dar ao luxo de gerar tantos métodos pois cada um precisa de uma classe e um classloader novos, ou seja, abusa da PermGen do Java.
Apesar de inline caches resultarem em ganho expressivo de performance, estão longe de produzirem algo razoável. O grande problema continua sendo o enorme custo de dispatch para código simples. A chave disso é fazer inferência dos tipos, de forma a conseguir eliminar por completo o overhead dos caches e resolução de nomes. As atuais implementações não implementam se quer inferência estática, ou seja, código como “123.to_s” é executado sem qualquer conhecimento prévio de 123.
O grande avanço ocorre se fizemos inferência em tempo de execução. Ou seja, instrumentamos o código para coletar os tipos que aparecem pelo código durante a execução e baseado nisso a máquina virtual gera versões mais eficientes do código. Existem duas técnicas bastante difundidas de como fazer isso, uma é via especialização parcial de métodos e a outra é via trace-based optimization.
Trace-based optimization é a tecnologia adotada pela Tamarin, a próxima VM de Javascript da Mozilla. De maneira sucinta, essa técnica consiste em gravar os trechos, e os tipos encontrados, que executam mais freqüentemente e gerar código eficiente baseado nessa informação. Os trechos gravados costumam incluir vários métodos diferentes e suas ativações juntas. Suas principais vantagens é a conseguir fazer inlining de métodos de maneira muito eficiente e normalmente gastar menos tempo no JIT. Porém existem uma enorme quantidade de problemas complexos de serem resolvidos como limitar expansão descontrolada da quantidade de código gerado.
Especialização parcial de métodos também leva em conta os métodos que executam mais freqüentemente. Os tipos dos parâmetros e valores de retorno
são armazenados e posteriormente utilizados para gerar versões especializadas dos métodos em questão. Sua principal vantagem é a maior simplicidade
e o fato de existir muito mais literatura e ferramentas para gerar código eficiente nestes casos. Para se ter uma idéia do poder dessa técnica um exemplo cai bem:
//ruby
def fun (a, b)
2 * a - b
end
//fun é sempre chamada com números como argumento, "fun (1,2)" por exemplo.
//pseudo-código C# gerado inicialmente (sem usar caches)
object fun_v0 (object a, object b) {
object tmp = Invoke (2, "*", a"); //_this, nome do método, argumentos
return Invoke (tmp, "-", b);
}
//pseudo-ćodigo C# gerado após especialização:
int fun_int_int (int a, int b) {
int tmp = 2 * a; //o método que implementava multiplicação foi inlined
return tmp - b;
}
object fun_v1 (object a, object b) {
if (a.Type == typeof (int) && b.Type == typeof (int) && typeof (int).version == 1)
return fun_int_int ((int)a, (int)b);
object tmp = Invoke (2, "*", a"); //_this, nome do método, argumentos
return Invoke (tmp, "-", b);
}
Não precisa ir muito longe para imaginar a diferença de performance entre as duas versões. Porém entre descobrir os métodos as serem especializados e gerar código de máquina existe um Just-In-Time compiler que é, surpresa, surpresa, muito trabalhoso de ser escrito para executar rapidamente e gerar código eficiente.
Possui um bom JIT é atualmente um grande dilema entre as implementações de ruby. Pois ao usar o JIT de máquinas virtuais maduras, como HotSpot ou mono, a implementação fica limitada ao que é possível a linguagens gerenciadas. Em contrapartida, ao não utilizá-las, construções de baixo nível como profiler e interpretador podem ser implementadas de forma muito mais eficiente.
Apesar de do futuro parecer muito legal, futuras VMs terão o trabalho adicional de educar a comunidade de desenvolvedores sobre como escrever código rápido em ruby. Coisas como monkey patching furam qualquer esquema de caching ou especialização pois cada objeto passa a ter uma singleton class distinta.
Não existe solução fácil e todas implementações tem muito chão pela frente até Ruby ter uma performance competitiva com outras linguagens dinâmicas. Soluções como as aqui apresentadas devem conseguir melhorias de uma ordem de magnitude tranquilamente, de acordo com os resultados já encontrados. Performance é um problema que é resolvido via muito suor, com uma contínua série de melhoras, atacando um pouco por vez.