Mês: fevereiro 2014

Linguagens compiladas, interpretadas e byte-code

Introdução

Recentemente, comecei a aprender Python. Tudo começou num bate-papo no café do TDC 2013, com o @juanplopes (thanks!). Estávamos discutindo sobre linguagens, ele mencionou o Python e eu imediatamente falei sobre minha “birra” com linguagens interpretadas. Então o Juan comentou de algumas alternativas de JIT-Compiler para o Python.

Esse post nasceu para falar de duas coisas: 1) Minha birra, 2) JIT-Compiler para o Python.

Um resuminho sobre linguagens compiladas, interpretadas e byte-code

Não vou me aprofundar nesse tema, que dá muita discussão, mas a idéia é só fazer uma diferenciação básica entre os 3 mundos.

Linguagens interpretadas em geral carregam todo o seu interpretador na memória, e conforme vão executando, traduzem as instruções de sua linguagem para instruções em linguagem de máquina. Um exemplo disso, é o velho VB (falo do VB6 ou ASP clássico, não do VB.NET). Quando você faz uma página ASP, simplesmente coloca ela no servidor e ela “funciona”. Já o VB clássico, é um pouco diferente. Como ele produz um “EXE” ele faz você pensar que é compilado, mas na verdade é o interpretador do VB, com teu código praticamente todo embutido como resource dentro dele. O antigo Clipper era assim também. Já o dBase (!) era um interpretador puro mesmo.

Linguagens compiladas, na sua etapa de compilação, traduzem o código fonte em linguagem de máquina. O binário (linguagem de máquina) não passa por toda a etapa de interpretação em tempo de execução, o que faz com que a execução seja muito mais rápida. O C, C++, Object Pascal (Delphi) são assim. Em geral, linguagens compiladas também são “não gerenciadas”, o que significa na prática que você deve controlar a gestão da sua memória por conta própria, o que traz muito controle e muito trabalho para o desenvolvedor.

A idéia do byte-code, veio para achar um meio-termo entre os dois mundos. O C#, VB.NET e Java são assim. Em tempo de compilação, você traduz da linguagem nativa (C#, Java) para uma linguagem intermediária. Assim são os assemblies .NET e os .class no java. Em tempo de execução, o framework traduz as instruções de byte-code para linguagem de máquina, porém, faz isso uma vez só. Esse é o processo do “JIT-Compiler” (just-in-time compiler). Em geral, a primeira execução é lenta pois passa pela compilação, mas nas etapas subsequentes, o tempo é mais ou menos parecido com uma linguagem compilada. Linguagens com byte-code em geral são “gerenciadas”, ou seja, possuem um garbage collector e cuidam de sua própria gestão de memória, o que por um lado diminui o trabalho do desenvolvedor, mas também diminui o controle do desenvolvedor sobre a gestão da memória (característica desejável para alguns tipos de aplicação).

Qual o problema com linguagens interpretadas?

O desempenho. Desconheço casos práticos em que linguagens interpretadas conseguem ser mais rápidas que compiladas e byte-code. O overhead de interpretar as instruções e traduzir em tempo de execução é muito grande.

Comparando entre as compiladas e byte-code, a discussão é mais complexa. O pessoal de compiladas (C++) costuma justificar que em casos extremos consegue fazer otimizações que os byte-codes não conseguem, e o pessoal de byte-code justifica que os ambientes “aprendem” conforme a execução e podem melhorar conforme o processo roda mais.

Eu percebo que na prática, JIT é mais lento que linguagem nativa no caso médio, porém, o custo vem no trabalho que dá fazer gestão de memória em linguagens compiladas. Acho que ambas tem suas aplicações.

Um micro-benchmark

Micro-benchmarks são péssimos e legais ao mesmo tempo. Péssimos porque não dá pra avaliar o desempenho de uma linguagem/ambiente num único SO, com um único exemplo bobo. Legais porque ajudam a ter uma idéia.

Resolvi fazer um exemplo muito simples. Um bubble sort (algoritmo popularmente conhecido por ser uma porcaria e gastar muitos recursos computacionais) e fazer o coitado ordenar uma matriz que está em ordem decrescente (pior caso). Assim ele gera O(n^2) comparações. Implementei o mesmo algoritmo em Python, C# e C++.

As implementações seguem:

C#


    public static class BubbleSort2
    {
        public static void Sort<T>(IList<T> list) where T: IComparable
        {
            bool swap = true;
            while (swap)
            {
                swap = false;
                for (int i = 1; i < list.Count; i++)
                {
                    if (list[i].CompareTo(list[i - 1]) < 0)
                    {
                        T tmp = list[i];
                        list[i] = list[i - 1];
                        list[i - 1] = tmp;
                        swap = true;
                    }
                }
            }
        }
    }

        [TestMethod]
        public void TestBigBubbleSort()
        {
            List<int> l = new List<int>(20000);
            for (int i = 20000; i >= 0; i--)
                l.Add(i);
            BubbleSort2.Sort(l);
        }

C++

#include <vector>

inline void Swap(std::vector<int> *l, int p1, int p2){
	int tmp = l->at(p1);
	l->at(p1) = l->at(p2);
	l->at(p2) = tmp;
}

void Sort(std::vector<int> *l){
	bool switched = true;
	while (switched){
		switched = false;
		for (int i = 1; i < l->size() - 1; i++){
			if (l->at(i) < l->at(i - 1)){
				Swap(l, i, i - 1);
				switched = true;
			}
		}
	}
}

namespace AlgorithmsTests
{		
	TEST_CLASS(UnitTest1)
	{
	public:
		
		TEST_METHOD(TestBigBubbleSort)
		{
			std::vector<int> l = std::vector<int>();			
			for (int i = 20000; i >= 0; i--)
				l.push_back(i);
			Sort(&l);

		}

	};
}

Python


def swap(list, p1, p2):
    tmp = list[p1]
    list[p1] = list[p2]
    list[p2] = tmp       

def bubble_sort(list):
    switched = True
    while switched :
        switched = False
        for i in range(len(list) - 1) :
            if i + 1 > len(list) - 1 :
                break
            if list[i] > list[i + 1] :
                swap(list, i, i + 1)
                switched = True
                
            
import unittest
from algorithms.core import bubble_sort

class BubbleSortTests(unittest.TestCase):
        
    def test_big_bubble_sort(self):
        l = [x for x in range(20000, 0, -1)]
        bubble_sort.bubble_sort(l)
    
if __name__ == '__main__':
    unittest.main()
       

Tempos de execução

Linguagem Tempo (ms)
C# 7.000
C++ 874
Python 109.199

Entenderam a razão da minha bronca com linguagens interpretadas?

Curiosidade inútil: Compilando em Debug, o C# demora 10s e o C++ em 1 minuto!

PyPy para o resgate

O PyPy é um JIT-Compiler para o Python. Neste ambiente, temos o seguinte resultado:

Linguagem Tempo (ms)
C# 7.000
C++ 874
Python (CPython) 109.199
Python (PyPy) 4.393

Como vemos no exemplo, apresenta um tempo de execução melhor que o C#. Interessante, né?

Resta saber se é um ambiente de execução maduro, mas é o suficiente para despertar meu interesse pela linguagem. Breve poderemos ter mais posts por aqui.

Integração Contínua

Objetivo

Recentemente, numa das listas de discussão que participo (.NET Architects), surgiu a discussão sobre integração continua. Fiquei um pouco surpreso. Apesar de integração contínua ser uma prática um tanto quanto antiga (a primeira referência que conheço do assunto é do livro do Kent Beck, de 1999), muita gente ainda se assusta ao falar do tema.

Essa foi minha motivação para escrever um pouco sobre integração contínua (CI – Continuous Integration) aqui.

O que é integração contínua

Primeiro vamos tentar entender o que “não é” integração contínua. Antigamente (assim espero), os desenvolvedores costumavam trabalhar em “silos”. Cada um pegava um pedaço do sistema pra fazer, fazia no seu canto e colocava-se um tempo de “integração” nos cronogramas. Esse tempo era para fazer os ajustes necessários para o lego todo se encaixar. Por exemplo: Um fazia a tela, o outro fazia o servidor, o outro as procedures de banco.

É claro que muita coisa dava errada. Nem sempre os contratos eram definidos em detalhes, muitos aspectos eram deixados de lado e na hora de integrar tudo, era uma longa dor de cabeça.

A proposta da prática de integração contínua é que ao contrário de segurar as alterações e subi-las somente no final do projeto e integrar tudo ao mesmo tempo, as pessoas devem constantemente integrar o seu trabalho ao restante do time. Lembro-me no livro original do Beck que ele tinha a proposta inclusive das pessoas fazerem o build numa máquina separada da sua máquina de desenvolvimento, justamente para evitar o famoso “na minha máquina funciona”, que gerou um dos posts mais divertidos sobre o assunto: The Works on My Machine Certification Program

Em resumo, a idéia da prática é (tradução das principais partes do wikipedia):

  • Use um repositório de controle de versão: Não, não vale uma pasta na rede compartilhada, um pen-drive com o código fonte. É um software de controle de versão que gerencie concorrência, mantenha histórico, permita que você administre branches, etc.
  • Automatize o Build: Sim, montar o seu pacote, exatamente como deveria ser entregue para seu usuário final deveria ser um processo de “um passo”. Apertar um único botão, e do outro lado deveria sair um pacote pronto, com todas as dependências resolvidas
  • Faça o build auto-testável: É uma excelente idéia juntar seus testes unitários ou mesmo de aceitação com o seu processo de build
  • Todo commit deve disparar um build: Cada vez que qualquer coisa é alterada no repositório de controle de versão, um build deve ser disparado, obviamente, disparando todo o processo de empacotamento e testes, e claro, se você já estiver neste nível de maturidade, deploy
  • Mantenha o build rápido
  • Todos devem ver o resultado do último build

Como implementar

Felizmente, hoje existem ferramentas open source fantásticas (em sua maioria melhores do que pagas) para implementar todo esse processo.

Para controle de versão, sugiro consultar a última pesquisa sobre controle de versão e também o post sobre gestão de configuração e versões.

Para a automação e criação do servidor de integração contínua, sugiro os seguintes passos:

Instale um servidor de integração contínua

Suba uma instância do seu servidor favorito. Os mais populares são:

  • Jenkins: Open-source, mais popular na comunidade Java, mas serve para buildar qualquer tipo de código fonte
  • Team city: Da JetBrains, free até uma certa quantidade de projetos. Ferramenta fantástica, muito intuitiva
  • CruiseControl.NET: Começou com a ThoughtWorks, agora parece que a comunidade open source assumiu. Foi meu favorito durante anos, mas ainda demandava muita configuração “na mão”

Existem outros também, como o próprio Team Foundation Server, Rational Team Concert, mas em geral não são tão simples e populares como os supra-citados, que suportam uma série de ferramentas de controle de versão. Aliás, esse é um ponto importantíssimo. Verifique se o servidor suporta o controle de versão que você usa.

Se você usa cvs, svn, git, garanto que não terá muito sofrimento em configurar qualquer um deles. Se você usa, TFS, VSS, RTC e outros, o máximo que posso dizer é: boa sorte. Esses controles de versão tem uma interface de linha de comando muito pobre, o que torna bem chato fazer qualquer tipo de automação.

Crie um script de build

Se você vem de plataforma Microsoft, usar o MSBuild será “natural” para você. Veja o post MSBuild in a nutshell sobre como criar os builds.

O ant é o irmão mais velho, da comunidade Java.

Esses “toolkits” de build nada mais são do que ferramentas para automatizar a execução de atividades de linha de comando. Se você não sabe trabalhar com linha de comando, aqui está uma excelente motivação para aprender. Será inevitável trabalhar direito com linha de comando para mexer com automação de build.

Os toolkits também permitem criar “tasks” customizadas, de forma que você pode utilizar todo seu conhecimento no seu framework favorito para fazer as maluquices mais improváveis em builds.

A dicas sobre automação de builds:

  • Comece de forma muito simples e depois vá tornando mais completo. Primeiro, faça o fonte compilar. Já dá para ter ganhos absurdos só fazendo isso
  • Mesmo que seu projeto não tenha testes, deixar a “infra-estrutura” pronta para receber os testes é uma boa. Por infra estrutura digo, a casca do projeto de testes e a mecânica para invocar os testes no build. A medida que o time percebe que nunca mais um teste ficará vermelho, porque o build quebra, a integração contínua pode ser uma forte aliada para alavancar os testes unitários
  • Empacotamento: O build não é somente compilar, ele também significa reunir todas as dependências necessárias para rodar a aplicação. Isso engloba: scripts de base de dados, binários externos, instaladores, etc.
  • Métricas: O MSBuildCodeMetrics é um projeto que comecei justamente com esse intuito. A idéia é chegar num nível de automação que se alguém commitar um método com mais e X linhas ou com complexidade ciclomática superior a Y, o build quebra.
  • Manutenção de numeração de versão: Quando se ganha experiência, o processo de criar as “tags” corretamente e carimbar as versões, pode ser todo feito no servidor de integração contínua. A série automatizando o versionamento no build (Parte 1 e Parte 2) falam um pouco sobre esta idéia.

Como os builds rodam server-side, é muito importante ser disciplinado na hora de montar o servidor de build. Eu por exemplo, não instalo Visual Studio em servidor de build, justamente para que os builds quebrem ao menor sinal de que alguma dependência “estranha” foi inserida na aplicação.

Notificação do build

Mais importante do que o build quebrar ou ter sucesso, é o time saber disso! Não adianta nada o build quebrar e ninguém corrigir. Por isso, é imprescindível instalar um notificador.

Os servidores de integração contínua, geralmente mandam e-mails. Infelizmente desenvolvedores em geral não dão muita atenção para e-mails corporativos.

Eu recomendo a instalação de um notificador associado ao “system tray”. O CruiseControl.NET tem o “CCTray”, que faz isso muito bem. O Jenkins tem o hudsonTracker ou o próprio CCTray que funciona no Jenkins (particularmente uso o CCTray).

Vale a pena sair instalando de máquina em máquina o notificador.

As etapas da implantação da integração contínua

Primeiro vem a etapa da negação. Os tipos de #mimimi mais comuns que você ouvirá serão:

  • A ferramenta de controle de versão fez merge errado
  • Na minha máquina funciona
  • O servidor de build está com problema

Paciência. Vale a pena pegar caso a caso e mostrar um a um o que o desenvolvedor fez de errado. Uma vez que ele entenda, ele não vai querer passar a mesma vergonha duas vezes.

Depois da etapa da negação, vem a etapa da aceitação. Nessa etapa, as pessoas não criticam mais a estabilidade e a necessidade do processo de build, mas também não contribuem. O build fica lá quebrado, e ninguém se interessa por arrumar. É nessa hora que vale a pena discutir um pouco sobre “propriedade coletiva de código”. Se o build quebra, é problema de todos.

É importante combinar algumas regras para fazer a prática pegar. Já vi castigos físicos (não recomendado, porque eu preciso ser politicamente correto num blog), colocar dinheiro num pote (Ex.: R$1 a cada quebra de build e o dinheiro é usado depois para comprar guloseimas) e em último caso a autorização para voltar qualquer commit que quebra o build. Quebrou o build? Alguém vai lá e volta o commit e o cara tem o trabalho de se virar para colocar as coisas no lugar.

Depois da fase da aceitação, vem a fase da dependência. Desligue o servidor de build por um dia. Se ninguém reclamar, você ainda não atingiu seu objetivo. Minha experiência mostra que uma vez que o time ganhe a maturidade para trabalhar com integração continua, ele não consegue mais trabalhar sem. É a regra básica para convivência no condomínio.

Vantagens e Desvantagens

Já fiquei me perguntando várias vezes as desvantagens de trabalhar com integração contínua, e não encontrei. Realmente é uma prática, na minha opinião, básica, essencial para qualquer projeto de desenvolvimento de software.

E as vantagens? São inúmeras. Algumas delas:

  • Você pode chegar todo dia para trabalhar, atualizar o código, tendo a certeza que o fonte vai compilar e você pode começar o seu dia
  • A automação de deploy se torna viável, pois você consegue construir sua aplicação, com qualquer versão, em qualquer momento do tempo
  • Suas dependências ficam mais fáceis de monitorar. Você conhecerá todos os binários de que depende, todas as ferramentas que precisa instalar para sua aplicação funcionar, e obviamente, se alguém quebrar a regra, descobrirá rápido (porque o servidor de build não tem o componente não-autorizado instalado)
  • Seu time vai aprender a subir código fonte somente quando as coisas estiverem minimamente estáveis. Os ganhos de produtividade nisso são enormes. Ninguém fica parado esperando o outro corrigir a besteira que fez no código
  • Você ganha um “evento” para fazer qualquer automação no momento que alguém subiu alguma alteração. Com isso, consegue fazer valer uma série de regras como: métricas, consistência de scripts de banco, formatação de código, e o que mais achar necessário

Era isso.