为什么这个Haskell程序在使用-threaded编译时会表现奇怪?

时间:2015-06-24 21:40:55

标签: multithreading performance haskell parallel-processing ghc

考虑以下玩具程序,通过将字符替换应用于字典单词来强制使用密码哈希。字典顺序或并行遍历,在编译时由PARMAP符号触发。

import qualified Control.Parallel.Strategies as Strat
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as BS.Base16
import qualified Data.ByteString.Char8 as BS.Char8
import Data.Char (isLower, toUpper)
import Data.Maybe (listToMaybe)

variants :: String -> [String]
variants "" = [""]
variants (c:s) = [c':s' | s' <- variants s, c' <- subst c]
  where subst 'a' = "aA@"
        subst 'e' = "eE3"
        subst 'i' = "iI1"
        subst 'l' = "lL1"
        subst 'o' = "oO0"
        subst 's' = "sS$5"
        subst 'z' = "zZ2"
        subst x | isLower x = [x, toUpper x]
        subst x = [x]

myMap :: (a -> [a]) -> [a] -> [[a]]
#ifdef PARMAP
myMap = Strat.parMap (Strat.evalList Strat.rseq)
#else
myMap = map
#endif

bruteForce :: [String] -> BS.ByteString -> Maybe String
bruteForce dictionary hash = listToMaybe $ concat $ myMap match dictionary
  where match word = [var | var <- variants word,
                      SHA256.hash (BS.Char8.pack var) == hash]

main :: IO ()
main = do
  dictionary <- lines `fmap` (readFile "dictionary.txt")
  hash <- (fst . BS.Base16.decode . BS.Char8.pack) `fmap` getLine
  case bruteForce dictionary hash of
    Just preimage -> putStrLn preimage
    Nothing -> return ()

我使用和不使用PARMAP-threaded编译此程序。

$ ghc Brute.hs -cpp -fforce-recomp -Wall -O2 -o brute.seq
$ ghc Brute.hs -cpp -fforce-recomp -Wall -O2 -DPARMAP -o brute.par
$ ghc Brute.hs -cpp -fforce-recomp -Wall -O2 -threaded -o brute.seq+th
$ ghc Brute.hs -cpp -fforce-recomp -Wall -O2 -threaded -DPARMAP -o brute.par+th

要运行这个程序,我会制作一个小字典并从中取出最后一个字。

$ shuf -n 300 /usr/share/dict/american-english >dictionary.txt
$ tail -n 1 dictionary.txt 
desalinates
$ echo -n 'De$aL1n@teS' | sha256sum
3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072  -

我在2核CPU上运行它。此计算机上没有运行其他CPU密集型进程。

顺序地图版本按预期执行。

$ TIMEFORMAT='real %R   user %U   sys %S'
$ time ./brute.seq <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 39.797   user 39.574   sys 0.156
$ time ./brute.seq+th <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 44.313   user 44.159   sys 0.088
$ time ./brute.seq+th +RTS -N <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 44.990   user 44.835   sys 0.876

没有-threaded编译的并行映射版本具有相同的性能。

$ time ./brute.par <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 39.847   user 39.742   sys 0.056

当我将并行地图与-threaded结合使用,但尚未使用2个内核时,事情开始变得奇怪了。

$ time ./brute.par+th <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 58.636   user 73.661   sys 17.717

当我使用2个内核时,事情变得更加奇怪了。现在,性能随着运行的不同而变化很大,而以前的版本没有表现出来。有时它的速度是单核brute.par+th的两倍,这正是我所期望的,因为该算法是令人尴尬的并行。但有时它甚至比在一个核心上慢。

$ time ./brute.par+th +RTS -N <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 28.589   user 51.615   sys 2.304
$ time ./brute.par+th +RTS -N <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 59.149   user 106.255   sys 4.664
$ time ./brute.par+th +RTS -N <<<3c76d2487f8094a8bf4cbb9e7de7624fab48c3d4f82112cb150b35b9a1cb9072
De$aL1n@teS
real 49.428   user 87.193   sys 3.816

现在我有两个可能相关的问题。

  1. 为什么1核brute.par+th比1核brute.par慢得多?它在那些线程中做了什么?它在内核模式下做了多少17秒?
  2. 为什么2核brute.par+th的性能变化如此之大,而不是可靠性是1核brute.par+th性能的两倍?
  3. 我正在使用GHC 7.4.1和parallel-3.2.0.2。我知道我应该使用更新的版本,但这是我目前使用的方法。

    我尝试使用-rtsopts进行编译并使用+RTS -qg禁用线程GC,但不起作用。

    我尝试过ThreadScope,但是它像疯了一样交换,即使我使用的是更小的字典也无法加载事件日志。

1 个答案:

答案 0 :(得分:3)

“Crypto.Hash.SHA256”调用不安全的FFI code这一事实可以解释意外的表现。 GHC不会guarantee在调用此代码期间不会阻止其他Haskell线程。如果生成的线程被GHC阻止,则会导致程序中出现大量争用,从而导致运行时间长且运行时间结果不一致。