这应该并行运行吗?
不,因为默认情况下 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 -> 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
现在,这比您以前的程序更容易阅读。嗯。您生成一个列表,然后应用三个过滤器。等一等。 sum1000 和 smaller 似乎关闭了。对于任何给定的范围,满足smaller 的三元组的数量通常相对较少,对于任何给定的a 和b,只有一个 c 满足sum1000 !
我们可以将这两个条件融合在一起,直接在a、b 和c 上得到以下条件:
-
a 永远不能大于 332,因为我们不能将 1000 - 333 拆分为 b 和 c,因此 smaller 仍然成立 (667 = 333 + 334)
-
b 总是大于 a
-
b永远不能大于(1000 - a) / 2,否则没有合适的c
-
c 始终是 1000 - a - b,但对于 a = 0 和 b = 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 < 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
将工作减少到其原始大小的微小总是胜过并行运行太多工作。