Por que todas as linguagens não têm a mesma eficiência?

4

Acabei de terminar meu curso de compiladores. Um dos tópicos discutidos foi maneiras de tornar os compiladores mais eficientes. Por exemplo: recursão de cauda, procedimentos de inlining, redução de força, remoção de código morto, propagação constante. Considerando que o principal motivo pelo qual o C é mais eficiente que o Python é porque ele não possui tipos dinâmicos e um coletor de lixo. Essas desvantagens podem ser removidas durante a fase de otimização quando o código de máquina real é gerado. Então, por que, no final do dia, uma linguagem como o C é mais eficiente que o Python?

    
por nikolaevra 03.08.2017 / 18:49
fonte

2 respostas

12

O código da máquina não torna a verificação de tipo magicamente irrelevante. A coisa com muitas variedades de código de máquina é que eles não têm compreensão inerente de tipos. Mas isso não significa que você não possa criar um sistema de tipos para eles usando suas próprias convenções.

Como um exemplo trivial, você pode decidir que cada valor é de 16 bits. Os primeiros 8 bits representam o tipo e os segundos 8 bits representam o valor real. Agora você tem algo que você pode verificar em tempo de execução para verificar se você não está adicionando um cavalo à sua latitude. Aqui está c=b+c no pseudo-assembly:

  enter               // function entry
  loadw [ax], bx      // load 16 bits at [ax] into bx
  loadw [ax+2], cx    // load 16 bits at [ax+2] into cx
  cmp   bl, cl        // compare the low bytes of bx and cx
  jne   ERROR         // if ^ is not equal, jump to ERROR
  addb  bh, ch        // add the high bytes of bx and cx, store in ch
  storw cx, [ax+2]    // store cx back in memory
  ret                 // return to caller
ERROR:
  // Handle error, print warning, throw exception, etc

Esse é um exemplo das instruções que uma linguagem dinamicamente tipper pode compilar (neste caso, uma linguagem realmente muito frágil; considere que mesmo se b e c forem iguais tipo, adição pode ser uma operação sem sentido para executar neles, por exemplo, GUID+GUID=huh? ). Veja o que uma linguagem com tipagem estática pode fazer:

  enter               // function entry
  loadb [ax], bh      // load 8 bits at [ax] into bh
  loadb [ax+1], ch    // load 8 bits at [ax+1] into ch
  addb  bh, ch        // add bh and ch, store in ch
  storb ch, [ax+1]    // store ch back in memory
  ret                 // return to caller

Observe que não há cmp , jne ou manipulação de erros. Por quê? Como uma linguagem estaticamente tipificada pode provar , sem executar o programa, que um par inválido de tipos nunca entrará nessa seção do código. Portanto, ele pode eliminar com segurança o código de verificação. E como ele não verifica os metadados de tipo extra, ele também pode deixar isso de lado, portanto, por que ele está apenas carregando e armazenando bytes de 8 bits em vez de palavras de 16 bits.

Da mesma forma, o código de máquina não limpa magicamente seu lixo para você. Se você estiver usando um coletor de lixo, ele não desaparecerá apenas quando estiver compilando, é traduzido para o idioma de destino juntamente com o resto do seu programa .

Mas observe que um coletor de lixo não é necessariamente menos eficiente que as alternativas. malloc() não é necessariamente determinista.

    
por 03.08.2017 / 20:02
fonte
5

Considering that the main reason why C would be more efficient than Python is because it doesn't have dynamic types and a garbage collector, all of those disadvantages will be removed after actual machine code is generated.

Não exatamente. Independentemente de você estar interpretando, executando a partir de uma árvore de análise ou código de byte intermediário ou código de máquina compilado, um programa Python ainda precisa se comportar como um programa Python. Isso significa que vem com toda a bagagem do Python.

Considere esta função e algum código que a chama:

def add(a, b):
    return a + b

print add(3, 2)
print add("Three", "Two")
print add("Five", 9)         # This will throw an exception.

O Python, que é digitado dinamicamente, não pode fazer suposições sobre os tipos de a e b quando algo chama add() . Internamente, a primeira chamada não é uma simples invocação de uma função com um par de constantes inteiras. Essas constantes na verdade têm pequenas tags ligadas a elas que dizem "isso é um inteiro". Dentro da função, algo tem que olhar para o que está no lado esquerdo do operador + , ler o tipo fora da tag, verificar se esse operador está definido para esse tipo e entregar ao operador ambas as expressões. O operador então tem que olhar para eles e decidir se o segundo argumento é compatível com o primeiro antes de tentar produzir um resultado. A segunda e a terceira chamadas funcionam exatamente da mesma maneira, exceto que é o tipo de string. O terceiro acaba tentando entregar o tipo string um inteiro para o operando direito, o tipo string decide que não pode adicioná-lo e lança uma exceção.

Essa é uma grande quantidade de decisões que precisam ser repetidas todas as vezes que algo chama add() .

Compiladores para linguagens com tipagem estática realmente passam pelo mesmo processo, mas conhecendo os tipos com antecedência, eles podem fazer toda a análise uma vez e gerar um código objeto agradável e compacto. Adicionar dois registros consome um ciclo ou dois. O exemplo na resposta do 8bittree ocupa muito mais e só digita segurança . Expanda-o para descobrir que parte do código precisa ser chamado antes de ser entregue a um tipo arbitrário e ele fica ainda mais longo.

    
por 03.08.2017 / 21:55
fonte