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

61

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

Por exemplo, escrevi um programa em Java que implementa um determinado protocolo. Neste protocolo, as duas partes enviam e recebem entre si milhares de mensagens, criptografam essas mensagens e as reenviam (e as recebem) repetidas vezes. Como esperado, o multithreading é fundamental 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, porque? É por causa do estilo de escrita? Se é por causa do estilo de escrita, agora que Java inclui expressões lambda, não consegui obter os mesmos resultados usando Java com lambda? Ou é mais rápido porque o Scala irá compilar as coisas de forma diferente?

    
por Aventinus 15.02.2016 / 16:17
fonte

4 respostas

94

A razão pela qual as pessoas dizem que as linguagens funcionais são melhores para o processamento paralelo é devido ao fato de que elas geralmente evitam o estado mutável. Estado mutável é a "raiz de todo o mal" no contexto do processamento paralelo; eles facilitam muito a obtenção de condições de corrida quando eles são compartilhados entre processos simultâneos. A solução para as condições de corrida envolve então mecanismos de bloqueio e sincronização, como você mencionou, que causam sobrecarga no tempo de execução, pois os processos aguardam uns pelos outros para usar o recurso compartilhado e maior complexidade de design, pois todos esses conceitos tendem a ser profundamente aninhados em tais aplicativos.

Quando você evita o 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 não terá a complexidade de design adicionada que geralmente segue.

No entanto, isso é tudo incidental. Se a sua solução em Java também evitar o estado mutável (especificamente compartilhada entre threads), convertê-la em uma linguagem funcional como Scala ou Clojure não traria benefícios 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 for 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 o estado mutável entre threads, causando condições de corrida ou adicionando a sobrecarga de sincronização para evitá-los.

    
por 15.02.2016 / 16:49
fonte
8

Classificar de ambos. É mais rápido porque é mais fácil escrever seu código de maneira mais fácil de compilar com mais rapidez. Você não terá necessariamente uma diferença de velocidade trocando de idioma, mas se você tivesse começado com uma linguagem funcional, você provavelmente teria feito o multithreading com muito menos esforço do programador . Na mesma linha, é muito mais fácil para um programador fazer erros de encadeamento que custam 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, livre de trava, em uma caixa tão pequena quanto possível, e escapar o mais rápido possível, de volta ao confortável mundo síncrono mutável. A maioria dos erros que custam sua velocidade são feitos 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.

    
por 15.02.2016 / 19:54
fonte
7

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

  1. A evitação do estado mutável tende a reduzir o número de coisas que podem dar errado em um programa, e ainda mais em um programa concorrente.
  2. A evitação de primitivos de sincronização de memória compartilhada e 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 vs. concorrência não determinística . Não há melhor explicação do que citando o excelente livro de Simon Marlow, Programação Paralela e Concorrente em Haskell ( as citações são de Capítulo 1 ):

A parallel program is one that uses a multiplicity of computational hardware (e.g., several processor cores) to perform a computation more quickly. The aim is to arrive at the answer earlier, by delegating different parts of the computation to different processors that execute at the same time.

By contrast, concurrency is a program-structuring technique in which there are multiple threads of control. Conceptually, the threads of control execute “at the same time”; that is, the user sees their effects interleaved. Whether they actually execute at the same time or not is an implementation detail; a concurrent program can execute on a single processor through interleaved execution or on multiple physical processors.

Além disso, as citações de Marlow também trazem a dimensão do determinismo :

A related distinction is between deterministic and nondeterministic programming models. A deterministic programming model is one in which each program can give only one result, whereas a nondeterministic programming model admits programs that may have different results, depending on some aspect of the execution. Concurrent programming models are necessarily nondeterministic because they must interact with external agents that cause events at unpredictable times. Nondeterminism has some notable drawbacks, however: Programs become significantly harder to test and reason about.

For parallel programming, we would like to use deterministic programming models if at all possible. Since the goal is just to arrive at the answer more quickly, we would rather not make our program harder to debug in the process. Deterministic parallel programming is the best of both worlds: Testing, debugging, and reasoning can be performed on the sequential program, but the program runs faster with the addition of more processors.

Em Haskell, os recursos de paralelismo e simultaneidade são projetados em torno desses conceitos. Em particular, o que outras linguagens agrupam como um conjunto de recursos, Haskell se divide em duas partes:

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

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

  1. Escreva uma função que produza uma lista de respostas, cada uma das quais é dispendiosa, 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 realmente computados até que um consumidor os exija.
  2. Use a biblioteca Estratégias para consumir os elementos das listas de resultados da sua função em paralelo em vários núcleos .

Eu realmente fiz isso com um dos meus programas de projetos de brinquedos há algumas semanas . Era trivial paralelizar o programa - a principal coisa que tive que fazer foi, na verdade, adicionar algum código que diz "calcule os elementos dessa lista em paralelo" (linha 90), e obtive um impulso de taxa de transferência quase linear em alguns dos meus casos de teste mais caros.

O meu programa é mais rápido do que se eu tivesse usado utilitários multithreading convencionais baseados em bloqueio? Eu duvido muito que sim. A coisa mais legal no meu caso foi ganhar tanto dinheiro com muito pouco dinheiro - meu código é provavelmente muito abaixo do ideal, mas como é tão fácil fazer paralelismo, tenho uma grande aceleração com muito menos esforço do que propriamente criar o perfil e otimizá-lo, e sem risco de condições de corrida. E isso, eu diria, é a principal maneira de a programação funcional permitir que você escreva programas "mais rápidos".

    
por 17.02.2016 / 03:35
fonte
2

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

Mesmo quando você precisa de variáveis de modificação, geralmente é possível usar sparely e junto com as variáveis não modificáveis. Outra coisa interessante no haskell é o STM, que substitui os 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 feita paralelamente para melhorar as coisas. desempenho-wise.

Isso torna o paralelismo em Haskell fácil na maior parte do tempo e, de fato, esforços estão em andamento 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 coisas que precisam ser avaliadas e as envia para todos os núcleos disponíveis, então você pode fazer um monte de "threads" na verdade não se tornam tópicos até que seja necessário. A ordem de avaliação, não importando, é característica da pureza, que geralmente requer programação funcional.

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

    
por 15.02.2016 / 19:37
fonte