Categoria: Python

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.