λ -calculus: Qual é a mais eficiente na representação de funções de memória?


9

Eu gostaria de comparar o desempenho das estruturas de dados codificadas por função (Church / Scott) versus classificadas (assembler / C).

Mas antes de fazer isso, preciso saber quão eficiente é / pode ser a representação de funções na memória. É claro que a função pode ser parcialmente aplicada (também conhecida como fechamento).

Estou interessado no uso das linguagens funcionais populares do algoritmo de codificação atual (Haskell, ML) e também no mais eficiente que pode ser alcançado.


Ponto de bônus: existe tal codificação que mapeia números inteiros codificados para números inteiros nativos ( short, intetc em C). Isso é possível?


Valorizo ​​a eficiência com base no desempenho. Em outras palavras, quanto mais eficiente a codificação, menos ela influencia o desempenho da computação com estruturas de dados funcionais.


Todas as minhas tentativas do Google falharam, talvez eu não conheça as palavras-chave corretas.
Ford O.

Você pode editar a pergunta para esclarecer o que você quer dizer com "eficiente"? Eficiente para quê? Quando você está solicitando uma estrutura de dados eficiente, precisa especificar quais operações deseja executar na estrutura de dados, pois isso afeta a escolha da estrutura de dados. Ou você quer dizer que a codificação é o mais eficiente em espaço possível?
DW

11
Isso é bem amplo. Existem muitas máquinas abstratas para o cálculo lambda que visam executá-lo com eficiência (veja, por exemplo, SECD, CAM, Krivine, STG). Além disso, é necessário considerar os dados codificados por Church / Scott, o que apresenta mais problemas. Por exemplo, nas listas codificadas pela Igreja, a operação da cauda deve ser O (n) em vez de O (1). Acho que li em algum lugar que a existência de uma codificação para listas no Sistema F com operações O (1) de cabeça e cauda ainda era um problema em aberto.
Chi

@ DW Estou falando sobre desempenho / despesas gerais. Por exemplo, com um mapeamento eficiente de codificação sobre a lista da igreja e a lista de Haskell, deve levar o mesmo tempo.
Ford O.

Desempenho para quais operações? O que você quer fazer com as funções? Deseja avaliar essas funções com algum valor? Uma vez, ou avalie a mesma função em muitos valores? Faça algo mais com eles? Você está apenas perguntando como compilar uma função (escrita em uma linguagem funcional) para que ela possa ser executada o mais eficiente possível?
DW

Respostas:


11

O fato é que não há muita margem de manobra em termos de codificação de funções. Aqui estão as principais opções:

  • Reescrita de termos: você armazena funções como suas árvores de sintaxe abstrata (ou alguma codificação das mesmas. Ao chamar uma função, você percorre manualmente a árvore de sintaxe para substituir seus parâmetros pelo argumento. Isso é fácil, mas extremamente ineficiente em termos de tempo e espaço) .

  • Encerramentos: você tem alguma maneira de representar uma função, talvez uma árvore de sintaxe, código de máquina mais provável. E nessas funções, você se refere aos seus argumentos por referência de alguma maneira. Pode ser um deslocamento de ponteiro, pode ser um número inteiro ou índice De Bruijn, pode ser um nome. Em seguida, você representa uma função como um fechamento : a função "instruções" (árvore, código etc.) emparelhada com uma estrutura de dados que contém todas as variáveis ​​livres da função. Quando uma função é realmente aplicada, ela sabe como procurar as variáveis ​​livres em sua estrutura de dados, usando ambientes, aritmética de ponteiros etc.

Tenho certeza de que existem outras opções, mas essas são básicas, e suspeito que quase todas as outras opções sejam uma variante ou otimização da estrutura básica de fechamento.

Portanto, em termos de desempenho, os fechamentos quase universalmente têm melhor desempenho do que a reescrita de termos. Das variações, qual é melhor? Isso depende muito da sua linguagem e arquitetura, mas suspeito que o "código de máquina com uma estrutura contendo vars livres" seja o mais eficiente. Ele tem tudo o que a função precisa (instruções e valores) e nada mais, e a chamada não acaba fazendo travessias de longo prazo.

Estou interessado no uso das linguagens funcionais populares do algoritmo de codificação atual (Haskell, ML)

Não sou especialista, mas sou 99% da maioria dos sabores de ML que usam algumas variações dos fechamentos que descrevo, embora com algumas otimizações prováveis. Veja isso para uma perspectiva (possivelmente desatualizada).

Haskell faz algo um pouco mais complicado por causa da avaliação preguiçosa: ele usa a reescrita de gráficos sem tags do Spineless .

e também no mais eficiente que pode ser alcançado.

O que é mais eficiente? Não existe uma implementação que seja mais eficiente em todas as entradas; portanto, você obtém implementações que são eficientes em média, mas cada uma se sobressai em diferentes cenários. Portanto, não há uma classificação definida de mais ou menos eficiente.

Não há mágica aqui. Para armazenar uma função, você precisa armazenar seus valores livres de alguma forma, caso contrário, você está codificando menos informações do que a própria função possui. Talvez você possa otimizar alguns dos valores livres com avaliação parcial, mas isso é arriscado para o desempenho, e você deve ter cuidado para garantir que isso sempre pare.

E talvez você possa usar algum tipo de compactação ou algoritmo inteligente para obter eficiência de espaço. Mas então você está trocando tempo por espaço ou está na situação em que otimizou para alguns casos e desacelerou para outros.

Você pode otimizar para o caso comum, mas o que o caso comum é possível alterar o idioma, área de aplicação, etc. O tipo de código que é rápido para um jogo de vídeo (processamento de números, laços apertados com grande entrada) é provavelmente diferente do que o que é rápido para um compilador (percursos em árvore, listas de trabalho etc.).

Ponto de bônus: existe tal codificação que mapeia números inteiros codificados para números inteiros nativos (short, int etc em C). Isso é possível?

Não, isso não é possível. O problema é que o cálculo lambda não permite a introspecção de termos. Quando uma função usa um argumento do mesmo tipo que um numeral da Igreja, precisa poder chamá-lo, sem examinar a definição exata desse numeral. É o que acontece com as codificações da Igreja: a única coisa que você pode fazer com elas é chamá-las, e você pode simular tudo de útil nisso, mas não sem um custo.

Mais importante, os números inteiros ocupam todas as codificações binárias possíveis. Portanto, se lambdas fossem representados como seus números inteiros, você não teria como representar lambdas que não sejam números de igreja! Ou, você introduziria um sinalizador para indicar se um lambda é um número ou não, mas qualquer eficiência que você deseja provavelmente desaparecerá pela janela.

EDIT: Desde que escrevi isso, tomei conhecimento de uma terceira opção para implementar funções de ordem superior: a defuncionalização . Aqui, toda chamada de função se transforma em uma grande switchdeclaração, dependendo de qual abstração lambda foi dada como função. A desvantagem aqui é que é uma transformação completa do programa: você não pode compilar partes separadamente e depois se vincular dessa maneira, pois é necessário ter o conjunto completo de abstrações lambda com antecedência.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.