【问题标题】:GHC Optimization: Collatz conjectureGHC 优化:Collat​​z 猜想
【发布时间】:2012-06-08 05:16:09
【问题描述】:

我已经在HaskellC++(ideone 链接)中为Project Euler's Challenge 14 编写了代码。他们都记得他们之前在数组中所做的任何计算。

分别使用ghc -O2g++ -O3,C++ 的运行速度比 Haskell 版本快 10-15 倍。

虽然我了解 Haskell 版本可能运行速度较慢,而且 Haskell 是一种更好的编写语言,但很高兴知道我可以对 Haskell 版本进行一些代码更改以使其运行得更快(最好在一个因素内) 2 或 3 个 C++ 版本)?


Haskell 代码在这里:

import Data.Array
import Data.Word
import Data.List

collatz_array = 
  let
    upperbound = 1000000
    a = array (1, upperbound) [(i :: Word64, f i :: Int) | i <- [1..upperbound]]
    f i = i `seq`
      let
        check_f i = i `seq` if i <= upperbound then a ! i else f i
      in
        if (i == 1) then 0 else (check_f ((if (even i) then i else 3 * i + 1) `div` 2)) + 1
  in a

main = 
  putStrLn $ show $ 
   foldl1' (\(x1,x2) (y1,y2) -> if (x2 >= y2) then (x1, x2) else (y1, y2)) $! (assocs collatz_array)

编辑:

我现在还使用未装箱的可变数组完成了一个版本。它仍然比 C++ 版本慢 5 倍,但有了显着的改进。代码在ideonehere

我想知道对可变数组版本的改进,使其更接近 C++ 版本。

【问题讨论】:

  • 仅供参考,使用 -fllvm 进行编译可将我的机器性能提高约 10%。
  • 您的seq 没有区别;你的两个函数在i 中都很严格。 GHC 以前在 32 位平台上 64 位算术很差劲,但我不知道你用的是什么平台。
  • 不能解释您的性能问题,但您的 C++(至少 nanothief 发布的内容)和 Haskell 代码都不会产生正确的答案。我无法编译您的 C++,但有一个与您的代码长度大致相同的纯 Haskell 解决方案,它在我的机器上快 25% 左右,并产生正确的结果。在这一点上,大约一半的时间看起来像是与启动 Haskell 程序相关的开销。
  • @PhilipJF 代码在多大程度上没有产生正确的结果?请注意,Clinton 使用了稍微不同的步骤,即对于奇数n,他直接转到(3*n+1)/2,而不是为此采取两个步骤。因此他得到不同的链长度,但最长链的起点是相同的。
  • @DanielFischer 没错,问题描述描述了链长,其中 (3n+1)/2 将长度增加 2。他的起点正确,但长度错误

标签: haskell optimization


【解决方案1】:

您的(可变数组)代码存在一些问题:

  • 您使用折叠来查找最大链长度,因为必须将数组转换为关联列表,这需要时间和分配 C++ 版本不需要。
  • 您使用evendiv 分别测试除以2。这些速度很慢。 g++ 将这两种操作都优化为更快的位操作(至少在据称更快的平台上),但 GHC 还没有进行这些低级优化,所以目前,它们必须手动完成.
  • 您使用readArraywriteArray。没有在 C++ 代码中完成的额外边界检查也需要时间,一旦处理了其他问题,这相当于运行时间的很大一部分(我的盒子上大约 25%),因为已经完成算法中有很多读写操作。

将其结合到实现中,我得到

import Data.Array.ST
import Data.Array.Base
import Control.Monad.ST
import Data.Bits

collatz_array :: ST s (STUArray s Int Int)
collatz_array = do
    let upper = 10000000
    arr <- newArray (0,upper) 0
    unsafeWrite arr 2 1
    let check i
            | upper < i = return arr
            | i .&. 1 == 0 = do
                l <- unsafeRead arr (i `shiftR` 1)
                unsafeWrite arr i (l+1)
                check (i+1)
            | otherwise = do
                let j = (3*i+1) `shiftR` 1
                    find k l
                        | upper < k = find (next k) $! l+1
                        | k < i     = do
                            m <- unsafeRead arr k
                            return (m+l)
                        | otherwise = do
                            m <- unsafeRead arr k
                            if m == 0
                              then do
                                  n <- find (next k) 1
                                  unsafeWrite arr k n
                                  return (n+l)
                              else return (m+l)
                          where
                            next h
                                | h .&. 1 == 0 = h `shiftR` 1
                                | otherwise = (3*h+1) `shiftR` 1
                l <- find j 1
                unsafeWrite arr i l
                check (i+1)
    check 3

collatz_max :: ST s (Int,Int)
collatz_max = do
    car <- collatz_array
    (_,upper) <- getBounds car
    let find w m i
            | upper < i = return (w,m)
            | otherwise = do
                l <- unsafeRead car i
                if m < l
                  then find i l (i+1)
                  else find w m (i+1)
    find 1 0 2

main :: IO ()
main = print (runST collatz_max)

以及时间安排(均为 1000 万):

$ time ./cccoll
8400511 429

real    0m0.210s
user    0m0.200s
sys     0m0.009s
$ time ./stcoll
(8400511,429)

real    0m0.341s
user    0m0.307s
sys     0m0.033s

看起来还不错。

重要提示:该代码仅适用于 64 位 GHC(因此,特别是在 Windows 上,您需要 ghc-7.6.1 或更高版本,以前的 GHC 即使在 64 -bit Windows),因为中间链元素超过 32 位范围。在 32 位系统上,必须使用 Integer 或 64 位整数类型(Int64Word64)来跟踪这些链,这会带来巨大的性能成本,因为原始的 64 位操作(算术和移位)作为对 32 位 GHC 中 C 函数的外部调用(快速 外部调用,但仍然比直接机器操作慢得多)实现。

【讨论】:

  • (3*h+1) shiftR` 1` 与(shiftR h 1) + h + 1 相同,在某些机器上可能更快
  • 确实如此。不会在我的身上产生可靠的可测量差异,所以如果有差异,它会小于这里的自然抖动。但在乘法速度较慢的机器上,这绝对值得一试。
【解决方案2】:

ideone 站点使用的是 ghc 6.8.2,它已经很老了。在 ghc 版本 7.4.1 上,差异要小得多。

使用 ghc:

$ ghc -O2 euler14.hs && time ./euler14
(837799,329)
./euler14  0.63s user 0.04s system 98% cpu 0.685 total

使用 g++ 4.7.0:

$ g++ --std=c++0x -O3 euler14.cpp && time ./a.out
8400511 429
./a.out  0.24s user 0.01s system 99% cpu 0.252 total

对我来说,ghc 版本只比 c++ 版本慢 2.7 倍。 此外,这两个程序给出的结果也不一样......(不是一个好兆头,尤其是对于基准测试)

【讨论】:

  • 糟糕,我发布的是 1000 万,而不是 100 万测试。链接已更正。请注意,c++ 版本的 1000 万比 Haskell 的 100 万快 2.7 倍。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多