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.
Sou fã de byte code. Gerenciamento manual de recursos na minha opinião fecal é um pouco masturbatorio, os SOs e os interpretadores evoluiram o suficiente para que a diferença nao seja gritante.
O Google está agora mesmo migrando de JIT para ART, e conseguiram espremer mais performance e economia de recursos sem obrigar todo o ecossistema a aprender gerenciamento de recursos. O controle e priorização ainda ficam por conta do SO, em linhas gerais eu diria que é até mais seguro.
É isso, esses são os meus dois centavos de opinião. Parabéns pelo post, muito informativo.
Muito boa a explicação!