【问题标题】:Should this run in parallel?这应该并行运行吗?
【发布时间】:2016-01-01 18:22:42
【问题描述】:

我的印象是 Haskell 会并行运行类似下面的程序(每个 a,b,c 组合将独立推过所有 filters)。

main = print $ 
       map (\(a,b,c) -> a * b * c) $
       filter (\(a,b,c) -> a^2 + b^2 == c^2) $
       filter (\(a,b,c) -> a + b + c == 1000) $
       filter (\(a,b,c) -> a < b && b < c) $
       [(a,b,c) | a <- [0..1000], b <- [0..1000], c <- [0..1000]]

但是当我运行程序时,我可以看到我机器上的四个线程中只有一个被使用了。

为什么我的预期是错误的?

【问题讨论】:

  • 使用runhaskell提供的Glasgow Haskell Compiler, Version 7.10.2运行代码
  • 你考虑过使用换行符吗? (它们不会帮助并行性,但它们会帮助每个人阅读您的代码。)
  • Haskell 不会自动并行计算。首先,它必须通过命令行选项(-threaded 或类似的东西)启用,但更重要的是,您必须告诉它要并行评估哪些内容,这在最基本的级别对应于 @ 中的 parparSeq 987654329@.
  • 附带说明,这可能是一个不好的例子..“并行”执行过滤器对您没有帮助,如果您按顺序执行它们,那么 ghc 会将所有过滤器和映射优化为一个邪恶的快速循环。如果你用par 将它们分开,我认为它无法进行这种优化。
  • 您在此处想要的那种并行性最好留给您友好的 CPU。您应该在这里做的主要事情是添加一个类型签名以强制这些数字具有类型Int,而不是默认为Integer。我还认为,如果您将所有过滤器融合为一个,代码会更简洁。

标签: haskell ghc


【解决方案1】:

这应该并行运行吗?

不,因为默认情况下 GHC 不添加并行性(见下文)。此外,并行性并不是一种方便的轨道炮,它可以解决任何类型的问题(见下文)。

为什么我的预期是错误的?

首先,使用runhaskell与使用GHCi基本相同:它不使用优化,因为-O--interactive冲突,它没有给你额外的RTS选项,你不能使用所有那些不错的编译器标志可以给你更多的汁液。

但即使您使用线程运行时编译代码,也不会得到更快的可执行文件:

$ ghc --make -O2 -rtsopts -with-rtsopts -N -thread SO.hs $ .\SO +RTS -s [31875000] 在堆中分配了 2,863,269,440 字节 GC 期间复制了 1,135,584 字节 100,016 字节最大驻留(2 个样本) 31,152 字节最大斜率 2 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 5471 colls,5471 par 0.266s 0.283s 0.0001s 0.0126s Gen 1 2 列,1 标准杆 0.000s 0.001s 0.0004s 0.0007s 并行GC工作平衡:0.00%(串行0%,完美100%) 任务:4(1 个绑定,3 个高峰工人(总共 3 个),使用 -N2) SPARKS:0(0 转换,0 溢出,0 无效,0 GC'd,0 失败) 初始化时间 0.000s(经过 0.001s) MUT 时间 20.328s(经过 21.671s)

这是因为 GHC 不会自动添加并行性。虽然只需拨动开关即可完成,但如果操作不当,并行性可能会导致相当高的开销。例如,如果f :: Int -&gt; T 是一个复杂的函数,那么运行head $ filter p $ parallelMap f [1..100] 可能就可以了。但是不再调用head $ filter p $ parallelMap f [1..]。毕竟,Haskell 是懒惰的。

在没有并行性的情况下加快速度

既然您知道为什么 Haskell 中没有自动并行性,那么您可以做些什么来加快您的程序?首先,构造它:

triples :: [(Int, Int, Int)]
triples = filter pythagoras . filter smaller . filter sum1000 $ ts
  where 
    pythagoras (a,b,c) = a ^ 2 + b ^ 2 == c ^ 2
    sum1000    (a,b,c) = a + b + c == 1000
    smaller    (a,b,c) = a < b && b < c

    ts = [(a,b,c) | a <- [0..1000], b <- [0..1000], c <- [0..1000]]

main :: IO ()
main = print $ map (\(a,b,c) -> a * b * c) $ triples

现在,这比您以前的程序更容易阅读。嗯。您生成一个列表,然后应用三个过滤器。等一等。 sum1000smaller 似乎关闭了。对于任何给定的范围,满足smaller 的三元组的数量通常相对较少,对于任何给定的ab,只有一个 c 满足sum1000

我们可以将这两个条件融合在一起,直接在abc 上得到以下条件:

  • a 永远不能大于 332,因为我们不能将 1000 - 333 拆分为 bc,因此 smaller 仍然成立 (667 = 333 + 334)
  • b 总是大于 a
  • b永远不能大于(1000 - a) / 2,否则没有合适的c
  • c 始终是 1000 - a - b,但对于 a = 0b = 500,没有 c

我们最终得到以下列表:

triples :: [(Int, Int, Int)]
triples = filter pythagoras . filter smaller . filter sum1000 $ ts
  where
    pythagoras (a,b,c) = a ^ 2 + b ^ 2 == c ^ 2
    sum1000    (a,b,c) = a + b + c == 1000
    smaller    (a,b,c) = a < b && b < c

    ts = [(a,b,c) | a <- [0..332]
                  , b <- [a+1 .. (1000 - a)`div` 2]
                  , let c = 1000 - a - b]

-- Old list for documentation
--  ts = [(a,b,c) | a <- [0..1000], b <- [0..1000], c <- [0..1000]]

您也可以删除过滤器,但不要忘记检查b &lt; c

这要快得多,因为我们现在使用的是 O(n²) 方法,而不是 O(n³) 方法。 runhaskell SO.hs 将在我的 PC 上 2 秒后完成,如果我们真的编译它,我们最终会得到一个几乎立即完成的可执行文件:

$ ghc --make -O2 SO.hs $ ./SO +RTS -s [31875000] 在堆中分配了 104,960 字节 GC 期间复制了 1,752 个字节 42,664 字节最大驻留(1 个样本) 18,776 字节最大斜率 1 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 0 colls,0 par 0.000s 0.000s 0.0000s 0.0000s Gen 1 1 colls, 0 par 0.000s 0.000s 0.0005s 0.0005s 初始化时间 0.000s(经过 0.001s) MUT 时间 0.000s(经过 0.002s)

TL;DR

将工作减少到其原始大小的微小总是胜过并行运行太多工作。

【讨论】:

  • 我在 SO 上收到的最惊人的答案,非常感谢!我没有对问题大小进行任何优化,因此我可以单独讨论并行性,但我明白减少它的意义。
  • 我是 Haskell 初学者,您的代码的可读性非常好。会给我一些目标。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-16
  • 2021-02-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多