Haskell Paralelo
(e entrada e saída de dados)
Prof. Fabrício Olivetti de França Universidade Federal do ABC
Nós já conhecemos o comando print, que imprime qualquer valor na tela.
Um comando menos genérico é o putStrLn.
Ele recebe uma String e retorna um IO ()?
Imprimindo na tela
Prelude> :t putStrLn putStrLn :: String -> IO ()GHCI
IO é o envelope da classe de entrada e saída de dados (Point, Just, …).
() representa uma unit, ou uma tupla sem elementos.
Vamos convencionar que IO é uma ação e IO () é uma ação que é executada e não retorna nada.
Da mesma forma temos a função getLine.
Ela espera por uma ação do usuário e armazena em name.
Imprimindo na tela
main = do
putStrLn "Please enter your name:"
name <- getLine
Diferente do que vimos até então, ela não
recebe parâmetros de entrada, apenas retorna uma String envolvida em IO.
Imprimindo na tela
Prelude> :t getLine
getLine :: IO String
Ações
Essas funções que recebem ou retornam IO são chamadas de ações pois queremos que sejam executadas no instante em que a
chamamos.
O retorno de um IO não pode ser previsto em tempo de compilação, não sabemos o que vai acontecer.
Ações
Considere a sequência: x <- getLine y <- getLine e: x = 13 y = 15Ações
Na segunda sequência, não importa a ordem de operação, mas na primeira a ordem importa. O operador <- executa a ação naquele
instante, garantindo a coerência do programa, e remove o envelope da ação.
Blocos "do"
Toda vez que precisamos executar sequências de operações (de modo imperativo), utilizamos um bloco do:
do
nome <- getLine putStrLn nome
Blocos "do"
O bloco do não pode terminar com uma operação <-, pois deve terminar com uma instrução pura.
do
nome <- getLine putStrLn nome
Purificando
Não se esqueça de que, se formos utilizar os valores provenientes do envelope IO, temos que purificá-los!
do
linha <- getLine
let x = read linha :: Integer -- converte em int, se possível
Lendo arquivos
Imagine o seguinte arquivo de dados, exemploData.txt: 1.2 3.5 2.3
4.1 2.1 3.4 ...
Lendo arquivos
Queremos ler seu conteúdo e transformar em uma lista de listas:
Lendo arquivos
Lê o arquivo em FilePath e retorna ele como string (envolvido em um IO).
file <- readFile "arquivo"
file se torna uma String com o conteúdo do arquivo.
Lendo arquivos
Vamos criar uma função parseFile que fará a conversão, a assinatura dela deve ser:
Lendo arquivos
Queremos que cada linha do arquivo seja uma lista de Doubles:
parseFile :: String -> [Double]
Lendo arquivos
A função parseLine converte cada palavra da linha em um Double:
parseFile :: String -> [Double]
parseFile file = map parseLine (lines file) where
parseLine l = map toDouble (words l) toDouble w = read w :: Double
Vamos verificar como a avaliação preguiçosa funciona no Haskell.
Para isso utilizaremos a função sprint no ghci que mostra o estado atual da variável.
Quero evitar a fadiga
GHCI
Prelude> :set -XMonomorphismRestriction
Prelude> x = 5 + 10
Prelude> :sprint x x = _
Quero evitar a fadiga
GHCI
Prelude> x = 5 + 10 Prelude> :sprint x x = _ Prelude> x 3 Prelude> :sprint x x = 3O valor de x é computado apenas quando requisitamos seu valor!
Quero evitar a fadiga
Prelude> x = 1 + 1 Prelude> y = x * 3 Prelude> :sprint x x = _ Prelude> :sprint y y = _Quero evitar a fadiga
Prelude> x = 1 + 1 Prelude> y = x * 3 Prelude> :sprint x x = _ Prelude> :sprint y y = _ Prelude> y 6 Prelude> :sprint x x = 2A função seq recebe dois parâmetros, avalia o primeiro e retorna o segundo.
Eu quero agora!
Prelude> x = 1 + 1 Prelude> y = 2 * 3 Prelude> :sprint x x = _ Prelude> :sprint y y = _ Prelude> seq x y 6 Prelude> :sprint x x = 2Quero evitar a fadiga
Prelude> let l = map (+1) [1..10] :: [Int] Prelude> :sprint l l = _ Prelude> seq l () Prelude> :sprint l l = _ : _ Prelude> length l Prelude> :sprint l l = [_,_,_,_,_,_,_,_,_,_] Prelude> sum l Prelude> :sprint l l = [2,3,4,5,6,7,8,9,10,11]
Considere a implementaçõa ingênua de fibonacci:
Um ponto óbvio para paralelizar: enquanto uma thread trabalha no (n-1) outra no (n-2).
O óbvio
fib 0 = 0fib 1 = 1
fib 2 = 1
Anotando paralelismo
import Control.Parallel
fib :: Integer -> Integer fib 0 = 0 fib 1 = 1 fib n = n1 `par` (n1 + n2) where n1 = fib (n - 1) n2 = fib (n - 2) main = do print (fib 36)
GHC
A função par indica para criar um spark para o primeiro argumento e executar o segundo
argumento. par x (x+y)
cria uma thread para calcular x e calcula x+y em paralelo.
Um spark é uma possibilidade de se tornar uma thread.
Se o programa julgar necessário transforma em thread.
Compile com:
ghc -o nome nome.hs -threaded -eventlog -rtsopts Execute com:
./nome +RTS -N1 -s -ls -M2g
-threaded: compile com suporte a multithreading -eventlog: permite criar um log do uso de threads -rtsopts: embute opções no seu programa
+RTS: flag para indicar opções embutidas -Nx: quantas threads usar
-s: estatísticas de execução
-ls: gera log para o threadscope
-M2g: limita o uso de memória em 2GB
Com 1 thread:
Total time 2.591s ( 2.620s elapsed) Com 2 threads:
Total time 2.749s ( 1.388s elapsed) O valor entre parênteses é o tempo real.
Anotando paralelismo
import Control.Parallel
fib :: Integer -> Integer fib 0 = 0 fib 1 = 1 fib n = n1 `par` (n2 + n1) where n1 = fib (n - 1) n2 = fib (n - 2) main = do print (fib 36)
GHC
Com 1 thread:
Total time 2.518s ( 2.541s elapsed) Com 2 threads:
Total time 4.475s ( 2.841s elapsed) O que houve??
No ghc o operador + avalia o operando da direita primeiro.
Enquanto uma thread avaliava n1 a principal avaliava também n1.
Anotando paralelismo
import Control.Parallel
fib :: Integer -> Integer fib 0 = 0
fib 1 = 1
fib n = n1 `par` n2 `pseq` (n2 + n1) where n1 = fib (n - 1) n2 = fib (n - 2) main = do print (fib 36)
GHC
pseq funciona como seq, porém aguarda que as threads terminem antes de continuar.
Com 1 thread:
Total time 2.845s ( 2.872s elapsed) Com 2 threads:
Total time 2.995s ( 1.523s elapsed)
Com o sincronismo, perdemos um tantinho de tempo, mas temos o paralelismo garantido.
Para avaliarmos se nossa estratégia de paralelismo está funcionando, temos a ferramenta threadscope:
$ cabal install threadscope
Fibonacci
Fibonacci
$ threadscope FibParSeq.eventlog
# de threads
Atividade thread 1 Atividade thread 2
Fibonacci
Vida de um spark
duds e fizzles
dud: já foi avaliado antes de virar uma thread fizzle: já foi avaliado por outra thread
Vida de um spark
Sinais de problemas:
- Poucos sparks, pode ser paralelizado ainda mais - Muitos sparks, paralelizando demais
Threadscope
Em breve veremos como usar o threadscope para melhorar nosso paralelismo.
Forma genérica de anotar os pontos a serem paralelizados.
Permite aplicar paralelismo em tipos compostos.
como paralelizar usando `par`?
Estratégias de Paralelismo
como paralelizar usando `par`?
Estratégias de Paralelismo
feio!
Estratégias de Paralelismo
Funções paralelas
parPair :: Strategy (a,b) parPair (a,b) = do
a' <- rpar a b' <- rpar b return (a',b')
Tipo Strategy de um tipo qualquer a é uma
função que recebe a e retorna a envelopado no tipo Eval.
O tipo Eval é um tipo que define o que e como deve ser avaliado.
rpar é uma função que retorna uma ação
envelopada em Eval (lembram de IO?).
Nesse caso precisamos utilizar a função return que envolve a tupla envelopada em Eval.
runEval executa a ação e retorna o valor fora
do envelope Eval.
Funções paralelas
Funções paralelas
using :: a -> Strategy a -> a x `using` s = runEval (s x)
Bonito! :)
Funções paralelas
parList, rseq, rdeepseq
parList - paraleliza o processamento de uma lista,
requer uma outra estratégia a ser aplicada em cada elemento.
rseq - força a avaliação de uma expressão dentro
daquele spark.
Cada elemento de l cria um spark que será avaliado via rseq.
Média
GHC
mean :: [[Double]] -> [Double]
mean l = map mean' l `using` parList rseq where
threadscope
Total time 1.381s ( 1.255s elapsed)
threadscope
Começamos a criar sparks muito rapidamente! Não deu tempo de aproveitá-los!
threadscope
Com o pool cheio, não deu tempo de enviar para o outro core! Então ficaram no core
Vamos tirar a estratégia...
Média
GHC
mean :: [[Double]] -> [Double]
mean l = map mean' l where
Agora dividimos a lista em pedaços de 1000 elementos e paralelizamos nesses pedaços.
Média
GHC
meanPar :: [[Double]] -> [Double]
meanPar l = concat lists where
lists = map mean chunks `using` parList rseq chunks = chunksOf 1000 l
threadscope
Total time 1.289s ( 1.215s elapsed)
threadscope
A aplicação da função mean foi preguiçosa, os sparks faziam apenas a promessa e
Vamos usar a estratégia rdeepseq.
Média
GHC
meanPar :: [[Double]] -> [Double]
meanPar l = concat lists where
lists = map mean chunks `using` parList rdeepseq chunks = chunksOf 1000 l
threadscope
Total time 1.303s ( 0.749s elapsed)