为什么"更好"数字列表功能更慢?

时间:2015-08-26 00:38:22

标签: performance haskell

我正在玩Project Euler#34,我写了这些函数:

import Data.Time.Clock.POSIX
import Data.Char

digits :: (Integral a) => a -> [Int]
digits x
    | x < 10 = [fromIntegral x]
    | otherwise = let (q, r) = x `quotRem` 10 in (fromIntegral r) : (digits q)

digitsByShow :: (Integral a, Show a) => a -> [Int]
digitsByShow = map (\x -> ord x - ord '0') . show

我认为肯定digits必须是更快的,因为我们不会转换为字符串。我不可能更错。我通过pe034运行了两个版本:

pe034 digitFunc = sum $ filter sumFactDigit [3..2540160]
    where
        sumFactDigit :: Int -> Bool
        sumFactDigit n = n == (sum $ map sFact $ digitFunc n)
        sFact :: Int -> Int
        sFact n
            | n == 0 = 1
            | n == 1 = 1
            | n == 2 = 2
            | n == 3 = 6
            | n == 4 = 24
            | n == 5 = 120
            | n == 6 = 720
            | n == 7 = 5040
            | n == 8 = 40320
            | n == 9 = 362880

main = do
    begin <- getPOSIXTime
    print $ pe034 digitsByShow -- or digits
    end <- getPOSIXTime
    print $ end - begin

使用ghc -O进行编译后,digits一直需要0.5秒,而digitsByShow则需要0.3秒。为什么会这样?为什么保持在整数运算中的函数更慢,而进入字符串比较的函数更快?

我问这个是因为我来自Java和类似语言的编程,其中生成数字的% 10技巧比&#34;转换为String&#34;更快。方法。我已经无法理解转换为字符串的速度可能更快。

2 个答案:

答案 0 :(得分:4)

这是我能想到的最好的。

digitsV2 :: (Integral a) => a -> [Int]
digitsV2 n = go n []
    where
      go x xs
          | x < 10    = fromIntegral x : xs
          | otherwise = case quotRem x 10 of
                  (q,r) -> go q (fromIntegral r : xs)

使用-O2进行编译并使用Criterion

进行测试

digits在470.4毫秒内运行

digitsByShow在421.8毫秒运行

digitsV2在258.0毫秒运行

结果可能会有所不同

编辑: 我不确定为什么建立像这样的列表有很大帮助。 但是,您可以通过严格评估quotRem x 10

来提高代码速度

您可以使用BangPatterns

执行此操作
| otherwise = let !(q, r) = x `quotRem` 10 in (fromIntegral r) : (digits q)

或案例

| otherwise = case quotRem x 10 of
                (q,r) -> fromIntegral r : digits q

执行此操作会将digits降至323.5毫秒

编辑:不使用标准的时间

digits = 464.3 ms

digitsStrict = 328.2 ms

digitsByShow = 259.2 ms

digitV2 = 252.5 ms

注意:标准包测量软件性能。

答案 1 :(得分:0)

让我们来研究为什么@No_signal's solution更快。

我做了三次ghc:

ghc -O2 -ddump-simpl digits.hs >digits.txt
ghc -O2 -ddump-simpl digitsV2.hs >digitsV2.txt
ghc -O2 -ddump-simpl show.hs >show.txt

digits.hs

digits :: (Integral a) => a -> [Int]
digits x
    | x < 10 = [fromIntegral x]
    | otherwise = let (q, r) = x `quotRem` 10 in (fromIntegral r) : (digits q)

main = return $ digits 1

digitsV2.hs

digitsV2 :: (Integral a) => a -> [Int]
digitsV2 n = go n []
    where
      go x xs
          | x < 10    = fromIntegral x : xs
          | otherwise = let (q, r) = x `quotRem` 10 in go q (fromIntegral r : xs)

main = return $ digits 1

show.hs

import Data.Char

digitsByShow :: (Integral a, Show a) => a -> [Int]
digitsByShow = map (\x -> ord x - ord '0') . show

main = return $ digitsByShow 1

如果你想查看完整的txt文件,我将它们放在ideone上(而不是在这里粘贴一个10000字符转储):

如果我们仔细查看 digits.txt ,看来这是相关部分:

lvl_r1qU = __integer 10

Rec {
Main.$w$sdigits [InlPrag=[0], Occ=LoopBreaker]
  :: Integer -> (# Int, [Int] #)
[GblId, Arity=1, Str=DmdType <S,U>]
Main.$w$sdigits =
  \ (w_s1pI :: Integer) ->
    case integer-gmp-1.0.0.0:GHC.Integer.Type.ltInteger#
           w_s1pI lvl_r1qU
    of wild_a17q { __DEFAULT ->
    case GHC.Prim.tagToEnum# @ Bool wild_a17q of _ [Occ=Dead] {
      False ->
        let {
          ds_s16Q [Dmd=<L,U(U,U)>] :: (Integer, Integer)
          [LclId, Str=DmdType]
          ds_s16Q =
            case integer-gmp-1.0.0.0:GHC.Integer.Type.quotRemInteger
                   w_s1pI lvl_r1qU
            of _ [Occ=Dead] { (# ipv_a17D, ipv1_a17E #) ->
            (ipv_a17D, ipv1_a17E)
            } } in
        (# case ds_s16Q of _ [Occ=Dead] { (q_a11V, r_X12h) ->
           case integer-gmp-1.0.0.0:GHC.Integer.Type.integerToInt r_X12h
           of wild3_a17c { __DEFAULT ->
           GHC.Types.I# wild3_a17c
           }
           },
           case ds_s16Q of _ [Occ=Dead] { (q_X12h, r_X129) ->
           case Main.$w$sdigits q_X12h
           of _ [Occ=Dead] { (# ww1_s1pO, ww2_s1pP #) ->
           GHC.Types.: @ Int ww1_s1pO ww2_s1pP
           }
           } #);
      True ->
        (# GHC.Num.$fNumInt_$cfromInteger w_s1pI, GHC.Types.[] @ Int #)
    }
    }
end Rec }

digitsV2.txt 的:

lvl_r1xl = __integer 10

Rec {
Main.$wgo [InlPrag=[0], Occ=LoopBreaker]
  :: Integer -> [Int] -> (# Int, [Int] #)
[GblId, Arity=2, Str=DmdType <S,U><L,U>]
Main.$wgo =
  \ (w_s1wh :: Integer) (w1_s1wi :: [Int]) ->
    case integer-gmp-1.0.0.0:GHC.Integer.Type.ltInteger#
           w_s1wh lvl_r1xl
    of wild_a1dp { __DEFAULT ->
    case GHC.Prim.tagToEnum# @ Bool wild_a1dp of _ [Occ=Dead] {
      False ->
        case integer-gmp-1.0.0.0:GHC.Integer.Type.quotRemInteger
               w_s1wh lvl_r1xl
        of _ [Occ=Dead] { (# ipv_a1dB, ipv1_a1dC #) ->
        Main.$wgo
          ipv_a1dB
          (GHC.Types.:
             @ Int
             (case integer-gmp-1.0.0.0:GHC.Integer.Type.integerToInt ipv1_a1dC
              of wild2_a1ea { __DEFAULT ->
              GHC.Types.I# wild2_a1ea
              })
             w1_s1wi)
        };
      True -> (# GHC.Num.$fNumInt_$cfromInteger w_s1wh, w1_s1wi #)
    }
    }
end Rec }

我实际上找不到 show.txt 的相关部分。我稍后会做的。

马上, digitsV2.hs 会产生更短的代码。这可能是一个好兆头。

digits.hs 似乎正在关注此伪代码:

def digits(w_s1pI):
    if w_s1pI < 10: return [fromInteger(w_s1pI)]
    else:
        ds_s16Q = quotRem(w_s1pI, 10)
        q_X12h = ds_s16Q[0]
        r_X12h = ds_s16Q[1]
        wild3_a17c = integerToInt(r_X12h)

        ww1_s1pO = r_X12h
        ww2_s1pP = digits(q_X12h)
        ww2_s1pP.pushFront(ww1_s1pO)
        return ww2_s1pP

digitsV2.hs 似乎正在关注此伪代码:

def digitsV2(w_s1wh, w1_s1wi=[]): # actually disguised as go(), as @No_signal wrote
    if w_s1wh < 10:
        w1_s1wi.pushFront(fromInteger(w_s1wh))
        return w1_s1wi
    else:
        ipv_a1dB, ipv1_a1dC = quotRem(w_s1wh, 10)
        w1_s1wi.pushFront(integerToIn(ipv1a1dC))
        return digitsV2(ipv1_a1dC, w1_s1wi)

可能不是这些函数像我的伪代码所暗示的那样改变列表,但这立即暗示了一些东西:它看起来好像digitsV2是完全尾递归的,而digits实际上不是(可能有使用一些Haskell蹦床或其他东西)。似乎Haskell需要将所有余数存储在digits中,然后将它们全部推到列表的前面,而它可以在digitsV2中推送它们并忘记它们。这纯粹是猜测,但这是有根据的推测。