A programação funcional é mais rápida no multithreading porque escrevo as coisas de maneira diferente ou porque as coisas são compiladas de maneira diferente?


63

Estou mergulhando no mundo da programação funcional e continuo lendo em todos os lugares que linguagens funcionais são melhores para programas multithreading / multicore. Eu entendo como as linguagens funcionais fazem muitas coisas de maneira diferente, como recursão , números aleatórios etc., mas não consigo descobrir se o multithreading é mais rápido em uma linguagem funcional porque é compilado de maneira diferente ou porque eu o escrevo de maneira diferente.

Por exemplo, eu escrevi um programa em Java que implementa um determinado protocolo. Nesse protocolo, as duas partes enviam e recebem umas às outras milhares de mensagens, criptografam essas mensagens e as reenviam (e as recebem) repetidamente. Como esperado, o multithreading é essencial quando você lida na escala de milhares. Neste programa não há bloqueio envolvido .

Se eu escrever o mesmo programa no Scala (que usa a JVM), essa implementação será mais rápida? Se sim, por que? É por causa do estilo de escrita? Se é por causa do estilo de escrita, agora que o Java inclui expressões lambda, não foi possível obter os mesmos resultados usando Java com lambda? Ou é mais rápido porque o Scala compilará as coisas de maneira diferente?


64
A programação funcional do Afaik não torna o multithreading mais rápido. Torna o multithreading mais fácil de implementar e mais seguro, porque existem alguns recursos da programação funcional, como imutabilidade e funções sem efeitos colaterais que ajudam nesse sentido.
Pieter B

7
Observe que 1) melhor não está realmente definido 2) certamente não está definido como simplesmente "mais rápido". Uma linguagem X que requer um bilhão de vezes o tamanho do código para obter um ganho de desempenho de 0,1% em relação a Y não é melhor que Y para qualquer definição razoável de melhor.
Bakuriu 15/02

2
Você quis perguntar sobre "programação funcional" ou "programas escritos em estilo funcional"? Frequentemente, a programação mais rápida não produz um programa mais rápido.
Ben Voigt

1
Não se esqueça que há sempre um GC que tem para executar em segundo plano e manter-se com suas exigências de alocação ... e eu não tenho certeza que é multithreaded ...
Mehrdad

4
A resposta mais simples aqui é: a programação funcional permite escrever programas que considerem menos problemas de condição de corrida, no entanto, isso não significa que os programas escritos no estilo imperativo serão mais lentos.
Dawid Pura

Respostas:


97

A razão pela qual as pessoas dizem que as linguagens funcionais são melhores para o processamento paralelo se deve ao fato de geralmente evitarem um estado mutável. Estado mutável é a "raiz de todo mal" no contexto do processamento paralelo; eles facilitam muito as condições de corrida quando são compartilhados entre processos simultâneos. A solução para as condições de corrida envolve mecanismos de bloqueio e sincronização, como você mencionou, que causam sobrecarga no tempo de execução, pois os processos esperam um pelo outro para fazer uso do recurso compartilhado e maior complexidade de design, pois todos esses conceitos tendem a ser profundamente aninhado em tais aplicativos.

Quando você evita um estado mutável, a necessidade de mecanismos de sincronização e bloqueio desaparece junto com ele. Como as linguagens funcionais geralmente evitam o estado mutável, elas são naturalmente mais eficientes e eficazes para o processamento paralelo - você não terá a sobrecarga de tempo de execução dos recursos compartilhados e a complexidade de design adicional que geralmente se segue.

No entanto, tudo isso é incidental. Se sua solução em Java também evitar um estado mutável (compartilhado especificamente entre threads), a conversão para uma linguagem funcional como Scala ou Clojure não trará nenhum benefício em termos de eficiência simultânea, porque a solução original já está livre da sobrecarga causada por os mecanismos de bloqueio e sincronização.

TL; DR: se uma solução no Scala é mais eficiente no processamento paralelo do que uma em Java, não é por causa da maneira como o código é compilado ou executado pela JVM, mas porque a solução Java está compartilhando um estado mutável entre os encadeamentos, causando condições de corrida ou adicionando a sobrecarga de sincronização para evitá-las.


2
Se apenas um encadeamento modificar um dado; nenhum cuidado especial é necessário. É somente quando vários encadeamentos podem modificar os mesmos dados que você precisa de algum tipo de cuidado especial (sincronização, memória transacional, bloqueio, o que for). Um exemplo disso é a pilha de threads, que é constantemente alterada pelo código funcional, mas não modificada por vários threads.
Brendan

31
Ter um thread modificando os dados enquanto outros o leem é suficiente para que você comece a tomar "cuidados especiais".
Peter Green

10
@Brendan: Não, se um thread modificar dados enquanto outros threads estiverem lendo esses mesmos dados, você terá uma condição de corrida. É necessário cuidado especial mesmo que apenas um segmento esteja modificando.
Cornstalks

3
O estado mutável é a "raiz de todo mal" no contexto do processamento paralelo => se você ainda não olhou para Rust, aconselho que você o espreite. Ele consegue permitir a mutabilidade de maneira muito eficiente, percebendo que o problema real é mutável misturado ao alias: se você tiver apenas um alias ou apenas uma mutabilidade, não haverá problema.
Matthieu M.

2
@MatthieuM. Certo, obrigado! Editei para expressar as coisas mais claramente na minha resposta. O estado mutável é apenas "a raiz de todo mal" quando é compartilhado entre processos concorrentes - algo que Rust evita com seus mecanismos de controle de propriedade.
22616 MichelHenrich

8

Tipo de ambos. É mais rápido, porque é mais fácil escrever seu código de uma maneira mais fácil de compilar mais rapidamente. Você não necessariamente terá uma diferença de velocidade alternando idiomas, mas se tivesse iniciado com uma linguagem funcional, provavelmente poderia ter feito o multithreading com muito menos esforço do programador . Na mesma linha, é muito mais fácil para um programador cometer erros de segmentação que custarão velocidade em uma linguagem imperativa e muito mais difícil perceber esses erros.

O motivo é que os programadores imperativos geralmente tentam colocar todo o código encadeado sem bloqueio na menor caixa possível e escapar o mais rápido possível, de volta ao seu confortável mundo síncrono e mutável. A maioria dos erros que lhe custam velocidade são cometidos nessa interface de limite. Em uma linguagem de programação funcional, você não precisa se preocupar tanto em cometer erros nesse limite. A maior parte do seu código de chamada também está "dentro da caixa", por assim dizer.


7

A programação funcional não cria programas mais rápidos, como regra geral. O que ele faz é facilitar a programação paralela e simultânea. Existem duas chaves principais para isso:

  1. Evitar o estado mutável tende a reduzir o número de coisas que podem dar errado em um programa, e mais ainda em um programa concorrente.
  2. Evitar a memória compartilhada e as primitivas de sincronização baseada em bloqueio em favor de conceitos de nível superior tende a simplificar a sincronização entre encadeamentos de código.

Um excelente exemplo do ponto 2 é que, em Haskell, temos uma clara distinção entre paralelismo determinístico versus concorrência não determinística . Não há explicação melhor do que citar o excelente livro de Simon Marlow, Parallel and Concurrent Programming in Haskell (as citações são do capítulo 1 ):

Um programa paralelo é aquele que usa uma multiplicidade de hardware computacional (por exemplo, vários núcleos de processador) para executar um cálculo mais rapidamente. O objetivo é chegar à resposta mais cedo, delegando diferentes partes da computação a diferentes processadores que são executados ao mesmo tempo.

Por outro lado, a simultaneidade é uma técnica de estruturação de programas na qual existem vários threads de controle. Conceitualmente, os threads de controle são executados “ao mesmo tempo”; isto é, o usuário vê seus efeitos intercalados. Se eles realmente são executados ao mesmo tempo ou não, é um detalhe da implementação; um programa simultâneo pode ser executado em um único processador através da execução intercalada ou em vários processadores físicos.

Além disso, Marlow menciona também traz a dimensão do determinismo :

Uma distinção relacionada é entre modelos de programação determinísticos e não determinísticos . Um modelo de programação determinístico é aquele em que cada programa pode fornecer apenas um resultado, enquanto um modelo de programação não determinístico admite programas que podem ter resultados diferentes, dependendo de algum aspecto da execução. Modelos de programação simultâneos são necessariamente não determinísticos, pois precisam interagir com agentes externos que causam eventos em momentos imprevisíveis. O não determinismo tem algumas desvantagens notáveis: os programas se tornam significativamente mais difíceis de testar e raciocinar.

Para programação paralela, gostaríamos de usar modelos de programação determinísticos, se possível. Como o objetivo é apenas chegar à resposta mais rapidamente, preferimos não tornar nosso programa mais difícil de depurar no processo. A programação paralela determinística é o melhor dos dois mundos: teste, depuração e raciocínio podem ser executados no programa seqüencial, mas o programa é executado mais rapidamente com a adição de mais processadores.

Em Haskell, os recursos de paralelismo e simultaneidade são projetados em torno desses conceitos. Em particular, que outros idiomas agrupam como um conjunto de recursos, Haskell se divide em dois:

  • Recursos determinísticos e bibliotecas para paralelismo .
  • Recursos não determinísticos e bibliotecas para simultaneidade .

Se você está apenas tentando acelerar uma computação determinística pura, ter paralelismo determinístico geralmente torna as coisas muito mais fáceis. Muitas vezes, você apenas faz algo assim:

  1. Escreva uma função que produza uma lista de respostas, cada uma das quais é cara de calcular, mas não depende muito uma da outra. Isso é Haskell, então as listas são preguiçosas - os valores de seus elementos não são computados até que um consumidor os exija.
  2. Use a biblioteca de estratégias para consumir os elementos das listas de resultados da sua função em paralelo em vários núcleos.

Na verdade, eu fiz isso com um dos meus programas de projetos de brinquedos há algumas semanas . Foi trivial paralelizar o programa - a principal coisa que tive que fazer foi, de fato, adicionar um código que diz "computar os elementos desta lista em paralelo" (linha 90), e obtive um aumento quase linear da taxa de transferência em alguns dos meus casos de teste mais caros.

Meu programa é mais rápido do que se eu tivesse usado utilitários multithreading convencionais baseados em bloqueio? Duvido muito. A coisa bacana no meu caso foi ganhar tanto dinheiro com tão pouco dinheiro - meu código provavelmente é muito abaixo do ideal, mas porque é tão fácil de paralelizar, consegui uma grande velocidade com muito menos esforço do que criar um perfil e otimizá-lo adequadamente, e sem risco de condições de corrida. E isso, eu diria, é a principal maneira pela qual a programação funcional permite que você escreva programas "mais rápidos".


2

Em Haskell, a modificação é literalmente impossível sem obter variáveis ​​modificáveis ​​especiais através de uma biblioteca de modificações. Em vez disso, as funções criam as variáveis ​​necessárias ao mesmo tempo que seus valores (que são computados preguiçosamente) e o lixo coletado quando não é mais necessário.

Mesmo quando você precisa de variáveis ​​de modificação, geralmente é possível usá-lo com moderação e com variáveis ​​não modificáveis. (Outra coisa interessante no haskell é o STM, que substitui bloqueios por operações atômicas, mas não tenho certeza se isso é apenas para programação funcional ou não.) Geralmente, apenas uma parte do programa precisará ser paralela para melhorar as coisas. em termos de desempenho.

Isso facilita o paralelismo em Haskell a maior parte do tempo e, de fato, estão sendo feitos esforços para torná-lo automático. Para código simples, o paralelismo e a lógica podem até ser separados.

Além disso, devido ao fato de que a ordem de avaliação não importa em Haskell, o compilador apenas cria uma fila de itens que precisam ser avaliados e os envia para quaisquer núcleos disponíveis, para que você possa criar um monte de "threads" que não importam na verdade, se tornam tópicos até necessário. A ordem de avaliação que não importa é característica da pureza, que geralmente requer programação funcional.

Leitura adicional:
Paralelismo em Haskell (HaskellWiki)
Programação simultânea e multicore em "Mundo Real Haskell"
Programação paralela e simultânea em Haskell por Simon Marlow


7
grep java this_post. grep scala this_poste grep jvm this_postnão retorne resultados :)
Andres F.

4
A pergunta é vaga. No título e no primeiro parágrafo, ele pergunta sobre programação funcional em geral , no segundo e terceiro parágrafo, pergunta sobre Java e Scala em particular . Isso é lamentável, especialmente porque um dos principais pontos fortes do Scala é precisamente o fato de que é não (apenas) uma linguagem funcional. Martin Odersky chama isso de "pós-funcional", outros chamam de "funcional de objeto". Existem duas definições diferentes do termo "programação funcional". Um deles é "programação com os procedimentos de primeira classe" (a definição original, tal como aplicado a LISP), o outro é ...
Jörg W Mittag

2
"programação com funções livres referenciamente transparentes, puras e sem efeitos colaterais e dados persistentes imutáveis" (uma interpretação muito mais rigorosa e também mais recente). Essa resposta aborda a segunda interpretação, que faz sentido, porque a) a primeira interpretação não tem relação alguma com paralelismo e concorrência, b) a primeira interpretação se tornou basicamente sem sentido, pois, com exceção de C, quase todas as línguas, mesmo em uso amplamente difundido hoje tem procedimentos de primeira classe (incluindo Java) ec) o OP pergunta sobre a diferença entre Java e Scala, mas não há ...
Jörg W Mittag

2
entre os dois em relação à definição nº 1, somente a definição nº 2.
Jörg W Mittag 15/02

A coisa da avaliação não é bem verdadeira como está escrita aqui; Por padrão, o tempo de execução não usa multithreading, e o IIRC, mesmo se você habilitar o multithreading, ainda precisará informar ao tempo de execução o que deve ser avaliado em paralelo.
cúbico
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.