【问题标题】:How to create Lists (or Array) of channels in Haskell, like Go如何在 Haskell 中创建通道列表(或数组),例如 Go
【发布时间】:2018-09-17 14:26:09
【问题描述】:

我正在尝试比较 Haskell 和 Go 的一些并发示例,以下代码是 Go 中使用 Goroutines 和通道的简单 map-reduce 示例。下面的 Go 代码计算平方和:

1^2+2^2+3^2....1024^2

为了测试 Go 和 Haskell 的性能,我重复计算 R 次 (10) 的平方和。

package main

import "fmt"

func mapper(in chan int, out chan int) {
    for v := range in {out <- v*v}
}

func reducer(in1, in2 chan int, out chan int) {
    for i1 := range in1 {i2 := <- in2; out <- i1 + i2}
}

func main() {
    const N = 1024  // calculate sum of squares up to N; N must be power of 2
    const R = 10  // number of repetitions to fill the "pipe"

    var r [N*2]chan int
    for i := range r {r[i] = make(chan int)}
    var m [N]chan int
    for i := range m {m[i] = make(chan int)}

    for i := 0; i < N; i++ {go mapper(m[i], r[i + N])}
    for i := 1; i < N; i++ {go reducer(r[i * 2], r[i *2 + 1], r[i])}

    go func () {
        for j := 0; j < R; j++ {
            for i := 0; i < N; i++ {m[i] <- i + 1} 
        }
    } ()

    for j := 0; j < R; j++ {
        <- r[1]
    } 
}

问题是如何在 Haskell 中高效地实现这个 mapreduce 示例。以下 Haskell 代码尝试在 main 函数中计算 10^2 + 7^2。我的问题是如何创建一个通道数组(或列表),例如 Go,然后在 main 函数中将映射器和减速器线程连接在一起。

import Control.Concurrent
data MRchannel = MRchannel !(MVar MRcmd)
data MRcmd = Pass !Int | Add !Int
  deriving (Show)

mapper:: MRchannel -> MRchannel -> IO ()
mapper left_C@(MRchannel left) right_C@(MRchannel right) = do
    v <- takeMVar left
    case v of
        Pass x -> do
            putMVar right (Add (x*x))
            mapper left_C right_C
        otherwise -> do
            putStrLn "Error!"
            return ()

reducer::  MRchannel -> MRchannel -> MRchannel -> IO ()
reducer left_1_C@(MRchannel left_1) left_2_C@(MRchannel left_2) 
right_C@(MRchannel right) = do
    v1 <- takeMVar left_1
    case v1 of
        Add x1 -> do
            v2 <- takeMVar left_2
            case v2 of
                Add x2 -> do 
                    putMVar right (Add (x1+x2))
                    reducer left_1_C left_2_C right_C
                otherwise -> do
                    putStrLn "Error!"
                    return ()
        otherwise -> do
            putStrLn "Error!"
            return ()

main = do
m1_l <- newEmptyMVar
m2_l <- newEmptyMVar
r1_l1 <- newEmptyMVar
r1_l2 <- newEmptyMVar
r1_r <- newEmptyMVar
    let m1_input = MRchannel m1_l
    let m2_input = MRchannel m2_l
    let r1_input1 = MRchannel r1_l1
    let r1_input2 = MRchannel r1_l2
    let r1_output = MRchannel r1_r
    forkIO $ mapper m1_input r1_input1
    forkIO $ mapper m2_input r1_input2
    forkIO $ reducer r1_input1 r1_input2 r1_output

    putMVar m1_l (Pass 10)
    putMVar m2_l (Pass 7)

    y <- takeMVar r1_r
    case y of 
        Add kvalue  -> do
            putStrLn $ show kvalue
        otherwise -> do
            putStrLn "Error"
            return () 

【问题讨论】:

  • 你能用文字描述你想计算什么吗?
  • GO 程序不打印任何东西;您答案中的haskell程序不断打印'358438400'。你能描述一下你想计算什么吗?那就是说;在我看来,正确的方法是利用 haskell 惰性(github.com/quchen/articles/blob/master/loeb-moeb.md)并在减速器中撒上par/pseq
  • 看看BoundedChan
  • 我正在尝试计算平方和:1^2+2^2+3^2....1024^2, (N * (N + 1) * (2 * N + 1)) / 6. 为了测试 Go 和 Haskell 的性能,我重复计算平方和 10 次 (R)。
  • @user2336525 MVar 允许您最多存储一个值。因此,如果生产者碰巧比消费者快,它就会阻塞。使用BoundedChan,您可以将更多值放入通道中,在这种情况发生时提供一些缓冲空间。 |请注意,对于性能比较,您可能会使用 parallelization rather than concurrency 获得更好的数字 - 虽然非常相似,但它们服务于完全不同的用例。

标签: list haskell


【解决方案1】:

我已经在 Haskell 中为这个 MapReduce 示例制定了一个解决方案。请指出此代码是否有任何部分可以优化。非常感谢您提供的任何帮助。

import Control.Concurrent

mapper:: (MVar Int) -> (MVar Int) -> IO ()
mapper left right = do
    v <- takeMVar left
    --putMVar right (v*v)
    putMVar right $! v*v
    mapper left right

reducer::  (MVar Int) -> (MVar Int) -> (MVar Int) -> IO ()
reducer left_1 left_2 right = do
    v1 <- takeMVar left_1
    v2 <- takeMVar left_2
    --putMVar right  (v1+v2)
    putMVar right $! v1+v2
    reducer left_1 left_2 right        

repeats:: Int -> [MVar Int] -> [MVar Int] ->IO ()
repeats 0 m r = do        
    --mapM_ (\ x -> putMVar (m!!x) (x+1) ) [0..1023]
    mapM_ (\ (e,x) -> putMVar e (x+1)) $ zip m [0..]
    result <- takeMVar (r!!1)
    return ()
    --putStrLn (show result)

repeats n m r = do
    --mapM_ (\ x -> putMVar (m!!x) (x+1) ) [0..1023]
    mapM_ (\ (e,x) -> putMVar e (x+1)) $ zip m [0..]
    result <- takeMVar (r!!1)
    --putStrLn (show result)
    repeats (n-1) m r

main = do
    --m <- sequence $ take 1024 $ repeat newEmptyMVar
    m <- replicateM 1024 newEmptyMVar
    --r <- sequence $ take 2048 $ repeat newEmptyMVar
    r <- replicateM 2048 newEmptyMVar

    mapM_ (\ x -> forkIO (mapper (m!!x) (r!!(1024+x) ))) [0..1023]
    mapM_ (\ x -> forkIO (reducer (r!!(x*2)) (r!!(x*2+1))  (r!!x))) [0..1023]
    repeats 1024 m r

【讨论】:

  • 次要观点:m &lt;- replicateM 1024 newEmptyMVar。要点:不要循环索引[0..1023],除非你真的必须 - 稍后使用!! 效率非常低(O(N ^ 2))!改用mapM_ (\ (e,x) -&gt; putMVar e (x+1)) $ zip m [0..],即 O(N)
  • @chi 非常感谢。我已根据您的建议修改了我的代码。它的运行速度比以前的实现快 10 倍。
【解决方案2】:

这里有一个替代方案。在我看来,它并不是非常优雅,但比原始代码要简单一些。

主要变化:

  • 我们没有两个MVars 列表并使用慢速!!
  • 我们使用forkAll 生成映射器/缩减器的整个二叉树(它的参数是深度:2^10 = 1024
  • forkAll 仍然不是最理想的,因为它使用的是速度较慢的 ++,尽管
  • 主要变化:在执行putMVar 时,我们强制使用$! 的值,这样我们就不会在MVar 中存储未计算的表达式。这是一个重大的性能提升。
  • 我们只打印它们的总和,而不是打印所有结果。否则,文本 IO 会降低基准测试速度。 (请注意,原来的repeats 会循环n+1,所以我必须使用10241 而不是下面的10240 来匹配结果。)

在这里,新代码的速度提高了 54%。

import Control.Concurrent
import Control.Monad

mapper :: MVar Int -> MVar Int -> IO ()
mapper left right = forever $ do
    v <- takeMVar left
    putMVar right $! v*v

reducer :: MVar Int -> MVar Int -> MVar Int -> IO ()
reducer left_1 left_2 right = forever $ do
    v1 <- takeMVar left_1
    v2 <- takeMVar left_2
    putMVar right $! v1+v2

forkAll :: Int -> MVar Int -> IO [MVar Int]
forkAll 0 res = do
  v <- newEmptyMVar
  _ <- forkIO $ mapper v res
  return [v]
forkAll depth res = do
  v1 <- newEmptyMVar
  r1 <- forkAll (depth - 1) v1
  v2 <- newEmptyMVar
  r2 <- forkAll (depth - 1) v2
  _ <- forkIO $ reducer v1 v2 res
  return (r1++r2)

main :: IO ()
main = do
  v <- newEmptyMVar
  res <- forkAll 10 v

  nums <- replicateM 10241 $ do
     mapM_ (\ (r,i) -> putMVar r (i+1)) $ zip res [0..]
     takeMVar v
  print $ sum nums

【讨论】:

  • 谢谢!这个新代码的初步结果是 8.823s。我使用“ghc -threaded -o mr-opt --make -O -rtsopts mr-opt.hs”编译它。不过我之前贴的代码是1.307s。你得到类似的结果吗?
  • 使用$!强制值后,结果从1.307s变为1.095s。
  • @user2336525 我只尝试使用-O 进行编译。如果没有优化,执行基准测试通常是没有意义的,因为可能会有很大的差异,就像在这种情况下一样。在我的电脑上,上面的代码运行时间约为 2.5 秒。原始代码运行时间约为 6 秒。
【解决方案3】:

这将返回与您的 Haskell 代码相同的内容;因为它是纯计算,所以它使用parallel 包。

import Data.Vector ((!), fromList, Vector)
import Control.Parallel (par, pseq)

reducer :: Int -> Int -> Int
reducer a b = a `par` b `pseq` a + b

result :: Vector Int -> Int
result src =
  let mr = fromList $
               map (\i arr -> reducer (arr ! (2 * i)) (arr ! (2 * i + 1))) [0..1023]
            ++ map (\i _ -> src ! i * src ! i) [0..1023]
            :: Vector (Vector Int -> Int)
      -- See https://github.com/quchen/articles/blob/master/loeb-moeb.md
      res = fmap ($ res) mr
  in res ! 1

main :: IO ()
main = do
  let src = fromList [1..1024]
  print (result src)

【讨论】:

    猜你喜欢
    • 2016-08-02
    • 2014-07-12
    • 2012-07-19
    • 2015-01-13
    • 2011-01-03
    • 2020-09-28
    • 2017-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多