关于性能:在na 整数分解中,Haskell比Python慢吗?

Haskell slower than Python in na?ve integer factorization?

我正在上数学课,我们必须做一些整数因式分解,作为解决问题的中间步骤。我决定编写一个python程序来为我做这件事(我们没有被测试我们的因素能力,所以这是完全超越董事会)。程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python3

import math
import sys

# Return a list representing the prime factorization of n. The factorization is
#   found using trial division (highly inefficient).
def factorize(n):

    def factorize_helper(n, min_poss_factor):
        if n <= 1:
            return []
        prime_factors = []
        smallest_prime_factor = -1
        for i in range(min_poss_factor, math.ceil(math.sqrt(n)) + 1):
            if n % i == 0:
                smallest_prime_factor = i
                break
        if smallest_prime_factor != -1:
            return [smallest_prime_factor] \
                   + factorize_helper(n // smallest_prime_factor,
                                      smallest_prime_factor)
        else:
            return [n]

    if n < 0:
        print("Usage:" + sys.argv[0] +" n   # where n >= 0")
        return []
    elif n == 0 or n == 1:
        return [n]
    else:
        return factorize_helper(n, 2)

if __name__ =="__main__":
    factorization = factorize(int(sys.argv[1]))
    if len(factorization) > 0:
        print(factorization)

我也在教自己一些haskell,所以我决定尝试在haskell中重写程序。程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import System.Environment

-- Return a list containing all factors of n at least x.
factorize' :: (Integral a) => a -> a -> [a]
factorize'
n x = smallestFactor
                 : (if smallestFactor == n
                    then []
                    else factorize' (n `quot` smallestFactor) smallestFactor)
    where
        smallestFactor = getSmallestFactor n x
        getSmallestFactor :: (Integral a) => a -> a -> a
        getSmallestFactor n x
            | n `rem` x == 0                          = x
            | x > (ceiling . sqrt . fromIntegral $ n) = n
            | otherwise                               = getSmallestFactor n (x+1)

-- Return a list representing the prime factorization of n.
factorize :: (Integral a) => a -> [a]
factorize n = factorize'
n 2

main = do
    argv <- getArgs
    let n = read (argv !! 0) :: Int
    let factorization = factorize n
    putStrLn $ show (factorization)
    return ()

(注意:这需要64位环境。在32位上,导入Data.Int并使用Int64作为read (argv !! 0)上的类型注释)

在我写下这篇文章之后,我决定比较这两个程序的性能,认识到有更好的算法,但是这两个程序使用的基本上是相同的算法。例如,我执行以下操作:

1
2
3
4
$ ghc --make -O2 factorize.hs
$ /usr/bin/time -f"%Uu %Ss %E" ./factorize 89273487253497
[3,723721,41117819]
0.18u 0.00s 0:00.23

然后,计时python程序:

1
2
3
$ /usr/bin/time -f"%Uu %Ss %E" ./factorize.py 89273487253497
[3, 723721, 41117819]
0.09u 0.00s 0:00.09

当然,每次我运行一个程序时,时间都会略有不同,但它们总是在这个范围内,因为Python程序比编译好的haskell程序快几倍。在我看来,haskell版本应该能够运行得更快,我希望你能给我一个如何改进它的想法,这样就可以了。

我已经看到了一些优化haskell程序的提示,就像这个问题的答案一样,但似乎不能让我的程序运行得更快。循环比递归快得多吗?Haskell的I/O是否特别慢?我在实际执行算法时犯了错误吗?理想情况下,我希望有一个Haskell的优化版本,它仍然相对容易阅读。


如果只计算一次limit = ceiling . sqrt . fromIntegral $ n,而不是每次迭代一次,那么我看到haskell版本更快:

1
2
3
4
5
6
7
limit = ceiling . sqrt . fromIntegral $ n
smallestFactor = getSmallestFactor x

getSmallestFactor x
    | n `rem` x == 0 = x
    | x > limit      = n
    | otherwise      = getSmallestFactor (x+1)

使用这个版本,我看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ time ./factorizePy.py 89273487253497
[3, 723721, 41117819]

real    0m0.236s
user    0m0.171s
sys     0m0.062s

$ time ./factorizeHs  89273487253497
[3,723721,41117819]

real    0m0.190s
user    0m0.000s
sys     0m0.031s

除了仙人掌的关键点之外,这里还有一些重构和严格注释的空间,以避免创建不必要的thunk。特别要注意,factorize是懒惰的:

1
factorize' undefined undefined = undefined : undefined

这并不是真正必要的,它迫使GHC分配几个thunk。其他地方的额外懒惰也是如此。我希望你能有更好的表现,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{-# LANGUAGE BangPatterns #-}

factorize' :: Integral a => a -> a -> [a]
factorize'
n x
  | smallestFactor == n = [smallestFactor]
  | otherwise = smallestFactor : factorize' (n `quot` smallestFactor) smallestFactor
  where
    smallestFactor = getSmallestFactor n (ceiling . sqrt . fromIntegral $ n) x
    getSmallestFactor n !limit x
       | n `rem` x == 0 = x
       | x > limit = n
       | otherwise = getSmallestFactor n limit (x+1)

-- Return a list representing the prime factorization of n.
factorize :: Integral a => a -> [a]
factorize n = factorize'
n 2

我让getSmallestFactorn和极限为论据。这样可以防止将getSmallestFactor分配为堆上的一个闭包。我不确定这是否值得额外的争论,你可以尝试两种方法。