为什么这个 Haskell 程序在使用 -threaded 编译时执行异常?
Why does this Haskell program perform strange when compiled with -threaded?
考虑以下玩具程序,它通过对字典单词应用字符替换来暴力破解密码哈希。字典按顺序或并行遍历,在编译时由 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"
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 核
brute.par+th
比 1 核 brute.par
慢这么多?它在这些线程中做什么?高达 17 秒的内核模式在做什么?
- 为什么 2 核
brute.par+th
的性能差异如此之大,而不是可靠地是 1 核 brute.par+th
性能的两倍?
我正在使用 GHC 7.4.1 和 parallel-3.2.0.2。我知道我可能应该使用较新的版本,但这是我目前手头上的东西。
我尝试使用 -rtsopts
进行编译并使用 +RTS -qg
禁用线程 GC,但没有任何效果。
我尝试了 ThreadScope,但它疯狂地交换并且无法加载事件日志,即使我使用了一个小得多的字典。
考虑以下玩具程序,它通过对字典单词应用字符替换来暴力破解密码哈希。字典按顺序或并行遍历,在编译时由 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"
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 核
brute.par+th
比 1 核brute.par
慢这么多?它在这些线程中做什么?高达 17 秒的内核模式在做什么? - 为什么 2 核
brute.par+th
的性能差异如此之大,而不是可靠地是 1 核brute.par+th
性能的两倍?
我正在使用 GHC 7.4.1 和 parallel-3.2.0.2。我知道我可能应该使用较新的版本,但这是我目前手头上的东西。
我尝试使用 -rtsopts
进行编译并使用 +RTS -qg
禁用线程 GC,但没有任何效果。
我尝试了 ThreadScope,但它疯狂地交换并且无法加载事件日志,即使我使用了一个小得多的字典。