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!