Compiladores como o Javac detectam automaticamente funções puras e as paralelizam?

12

As funções puras são conhecidas por facilitar a paralelização. O que é a programação funcional que a torna inerentemente adaptada à execução paralela?

Compiladores como Javac são inteligentes o suficiente para detectar quando um método é uma função pura? Pode-se sempre implementar classes que implementam interfaces funcionais como Function , mas têm efeitos colaterais.

Naveen
fonte
7
A questão não é apenas se o compilador pode saber se uma função é pura, mas também se pode agendar inteligentemente a execução paralela de funções puras. Não é suficiente disparar um novo thread para cada um: isso é ineficiente. O GHC (Haskell) lida com isso usando preguiça e "fios verdes"; Sinceramente, ficaria surpreso se alguma linguagem impura tentasse, dada a dificuldade adicional de garantir que os threads puros fossem agendados corretamente em relação ao thread impuro principal.
Ryan Reich
@RyanReich, há algum ganho de desempenho ao usar a programação funcional em uma linguagem funcional impura como Java? os ganhos da programação funcional são puramente funcionais, como a modularidade?
Naveen
O @RyanReich GHC lida com o problema fazendo com que o programador faça anotações quando quiserem paralelismo. Pureza implica que essas anotações nunca mudam a semântica, apenas o desempenho. (Também existem mecanismos de concorrência que podem dar origem ao paralelismo, mas essa é uma chaleira diferente de peixe.)
Derek Elkins saiu de SE em
@Naveen Existem outros benefícios para as funções puras no que diz respeito à otimização, além do paralelismo, como código de reordenamento de maior liberdade, memorização e eliminação de subexpressão comum. Eu posso estar errado, mas duvido que o javac tente detectar a pureza, pois provavelmente é bastante raro no código idiomático e um pouco difícil para todos, exceto os casos mais triviais. Por exemplo, você precisa saber que não haverá NullPointerExceptions. Os benefícios das otimizações com base nisso também são provavelmente bastante pequenos para aplicativos Java típicos.
Derek Elkins saiu de SE
6
javac é o compilador java, que pega o código-fonte java e gera arquivos de classe de código de java byte. É bastante restrito ao que ele pode (e deve) fazer. Ele não tem a liberdade ou os mecanismos subjacentes necessários para introduzir paralelismo no arquivo de classe de código de bytes.
Erik Eidt

Respostas:

33

são compiladores como Javac inteligentes o suficiente para detectar quando um método é uma função pura.

Não é uma questão de "inteligente o suficiente". Isso é chamado de Análise de Pureza e é comprovadamente impossível no caso geral: é equivalente a resolver o Problema da Parada.

Agora, é claro, os otimizadores fazem coisas comprovadamente impossíveis o tempo todo, "comprovadamente impossível no caso geral" não significa que nunca funcione, apenas significa que não pode funcionar em todos os casos. Portanto, de fato, existem algoritmos para verificar se uma função é pura ou não; é mais provável que o resultado seja "Não sei", o que significa que, por razões de segurança e exatidão, você deve assumir que essa função específica possa ser impura.

E mesmo nos casos em que faz o trabalho, os algoritmos são complexos e caros.

Então, esse é o problema nº 1: ele funciona apenas para casos especiais .

Problema # 2: Bibliotecas . Para que uma função seja pura, ela só pode chamar funções puras (e essas funções podem chamar apenas funções puras, e assim por diante). Obviamente, Javac sabe apenas sobre Java e apenas sobre código que pode ver. Portanto, se sua função chamar uma função em outra unidade de compilação, não será possível saber se é pura ou não. Se chamar uma função escrita em outro idioma, você não saberá. Se ele chama uma função em uma biblioteca que talvez ainda não esteja instalada, você não pode saber. E assim por diante.

Isso só funciona quando você tem uma análise do programa inteiro, quando o programa inteiro é escrito no mesmo idioma e tudo é compilado de uma só vez. Você não pode usar nenhuma biblioteca.

Problema nº 3: agendamento . Depois de descobrir quais peças são puras, você ainda precisa agendá-las para separar os segmentos. Ou não. Iniciar e parar threads é muito caro (especialmente em Java). Mesmo se você mantiver um conjunto de encadeamentos e não os iniciar ou parar, a alternância de contexto de encadeamento também será cara. Você precisa ter certeza de que o cálculo será executado significativamente mais do que o tempo necessário para agendar e alternar o contexto; caso contrário, você perderá o desempenho e não o obterá.

Como você provavelmente já adivinhou, descobrir o tempo que uma computação levará é comprovadamente impossível no caso geral (não podemos nem imaginar se levará um tempo finito, quanto mais tempo) e difícil e caro, mesmo em o caso especial.

Além: Javac e otimizações . Observe que a maioria das implementações do javac não realiza muitas otimizações. A implementação do javac da Oracle, por exemplo, depende do mecanismo de execução subjacente para fazer otimizações . Isso leva a outro conjunto de problemas: digamos, o javac decidiu que uma função específica é pura e é cara o suficiente e, portanto, a compila para ser executada em um encadeamento diferente. Em seguida, o otimizador da plataforma (por exemplo, o compilador HotSpot C2 JIT) aparece e otimiza toda a função. Agora, você tem um segmento vazio sem fazer nada. Ou, imagine, novamente, o javac decide agendar uma função em um encadeamento diferente, e o otimizador de plataforma pode otimize-o completamente, exceto que ele não pode executar inlining através dos limites do encadeamento e, portanto, uma função que poderia ser otimizada completamente agora é desnecessariamente executada.

Portanto, fazer algo assim só faz sentido se você tiver um único compilador fazendo a maioria das otimizações de uma só vez, para que o compilador conheça e possa explorar todas as otimizações diferentes em diferentes níveis e suas interações entre si.

Note-se que, por exemplo, o compilador HotSpot C2 JIT realmente faz executar alguma auto-vetorização, que também é uma forma de auto-paralelização.

Jörg W Mittag
fonte
Bem, dependendo da sua definição de "função pura", o uso de funções impuras na implementação pode ser permitido.
Deduplicator
@Deduplicator Bem, dependendo da sua definição de definition, usando um disparate definitionde purityprovavelmente é obscura
gato
1
Seu problema nº 2 é principalmente invalidado pelo fato de que praticamente todas as otimizações são executadas pelo JIT (você obviamente o conhece, mas o ignora). Da mesma forma, o problema nº 3 é parcialmente invalidado, pois o JIT depende muito das estatísticas reunidas pelo intérprete. Eu discordo especialmente de "Você não pode usar nenhuma biblioteca", pois há desoptimização para o resgate. Concordo que a complexidade adicionada seria um problema.
Maaartinus 9/07
2
@ maaartinus: Além disso, apenas o final da minha resposta é específico para o javac. I especificamente fazer menção, por exemplo, que "Isso só funciona, quando você tem a análise de todo o programa, quando todo o programa é escrito na mesma língua, e tudo é compilado em vez de uma só vez." Obviamente, isso é verdade para o C2: ele lida apenas com um idioma (JVM bytecode) e tem acesso a todo o programa de uma só vez.
Jörg W Mittag
1
@ JörgWMittag Eu sei que o OP pergunta sobre javac, mas eu aposto que eles estão assumindo que javac é o responsável pelas otimizações. E que eles mal sabem que existe C2. Não estou dizendo, sua resposta é ruim. É que deixar o javac fazer qualquer otimização (exceto trivialidades como o uso StringBuilder) não faz sentido, então eu o descartaria e simplesmente assumiria que o OP escreve javac, mas significa Hotspot. Seu problema # 2 é uma boa razão para otimizar qualquer coisa em javac.
Maaartinus 9/07
5

A resposta votada não conseguiu notar uma coisa. A comunicação síncrona entre threads é extremamente cara. Se a função for capaz de ser executada a uma taxa de muitos milhões de chamadas por segundo, será mais difícil paralelizá-la do que deixá-la como está.

Infelizmente, a forma mais rápida de comunicação síncrona entre threads, usando loops ocupados com variáveis ​​atômicas, é ineficiente em energia. Se você precisar recorrer ao uso de variáveis ​​de condição para economizar energia, o desempenho da sua comunicação entre threads é prejudicado.

Portanto, o compilador não precisa apenas determinar se uma função é pura, mas também estimar o tempo de execução da função para ver se a paralelização é uma vitória líquida. Além disso, seria necessário escolher entre loops ocupados usando variáveis ​​atômicas ou variáveis ​​de condição. E seria necessário criar tópicos nas suas costas.

Se você criar os encadeamentos dinamicamente, será ainda mais lento do que usar variáveis ​​de condição. Portanto, o compilador precisaria configurar um certo número de threads já em execução.

Portanto, a resposta para sua pergunta é não , os compiladores não são "inteligentes" o suficiente para paralelizar automaticamente funções puras, especialmente no mundo Java. Eles são inteligentes, não os paralelizando automaticamente!

juhist
fonte
5
" Eles são inteligentes por não os paralelizarem automaticamente! " : Isso vai longe demais. Embora seja verdade que paralelizar em todos os pontos possíveis apenas por si só seja geralmente ineficiente, um compilador inteligente identificaria uma estratégia prática de paralelização. Eu acho que a maioria das pessoas entende isso, então, quando falamos em auto-paralelização, queremos dizer auto-prática-paralelização.
Nat
@ Nat: Ridiculamente muito difícil. Isso exigiria a identificação de funções puras na escala de tempo de execução de 100s de milissegundos, e esperar que o compilador tenha alguma idéia do tempo de execução de loops que não tenham constantes em suas iterações (e os casos que você deseja não) são bobos.
Joshua
Eu concordo - o comentário de @ Nat implica que paralelização não significa necessariamente vários threads, o que é verdade. O JIT poderia, por exemplo, alinhar várias chamadas para uma função pura e intercalar suas instruções de CPU em certos casos. Por exemplo, se as duas chamadas de método buscarem uma constante, ela poderá ser buscada uma vez e mantida em um registro de CPU para ambas as instâncias do método a serem usadas. As CPUs modernas são bestas com vários registros de uso geral e instruções especializadas que podem ser bastante úteis na otimização do código.
1
@ Josué: Muito mais fácil para um compilador JIT. O compilador JIT também pode descobrir que uma função pode não ser pura, mas nenhuma chamada até agora invocou um comportamento impuro.
gnasher729
Eu concordo com @ Josué. Eu tenho um algoritmo difícil de paralelizar no trabalho. Eu tentei paralelizar manualmente, mesmo fazendo algumas aproximações simplificadoras (e, assim, modificando o algoritmo), e sempre falhei miseravelmente. Mesmo um programa que diga se é possível paralelizar algo é extremamente difícil, mesmo que seja muito mais simples do que realmente paralelizar. Lembre-se de que estamos falando de linguagens de programação completas de Turing.
21717 juhist