计算最小除数的两个替代函数的性能

时间:2014-02-23 16:31:57

标签: performance algorithm haskell primes

在“ The Haskell the Logic,Maths and Programming ”一书中,作者提出了两种替代方法,即找出k的最小除数n k > 1,声称第二个版本比第一个版本快得多。我有理解为什么(我是初学者)的问题。

这是第一个版本(第10页):

ld :: Integer -> Integer -- finds the smallest divisor of n which is > 1
ld n = ldf 2 n

ldf :: Integer -> Integer -> Integer
ldf k n | n `rem` k == 0 = k
        | k ^ 2 > n      = n
        | otherwise      = ldf (k + 1) n

如果我理解正确,ld函数基本上会迭代[2..sqrt(n)]区间内的所有整数,并在其中一个除n后立即停止,并将其作为结果

第二个版本,作者声称要快得多,就像这样(第23页):

ldp :: Integer -> Integer -- finds the smallest divisor of n which is > 1
ldp n = ldpf allPrimes n

ldpf :: [Integer] -> Integer -> Integer
ldpf (p:ps) n | rem n p == 0 = p
              | p ^ 2 > n    = n
              | otherwise    = ldpf ps n

allPrimes :: [Integer]
allPrimes = 2 : filter isPrime [3..]

isPrime :: Integer -> Bool
isPrime n | n < 1     = error "Not a positive integer"
          | n == 1    = False
          | otherwise = ldp n == n

作者声称此版本更快,因为它只在区间2..sqrt(n)内通过 primes 列表进行迭代,而不是遍历该范围内的所有数字。

然而,这个论点并没有让我信服:递归函数ldpf逐个吃掉素数列表allPrimes中的数字。通过在所有整数列表上执行filter来生成此列表。

因此,除非我遗漏了某些内容,否则第二个版本最终会迭代2..sqrt(n)区间内的所有数字,但对于每个数字,它首先检查它是否为素数(相对昂贵的操作),如果是,它检查它是否划分n(一个相对便宜的)。

我想说的是,检查每个k的{​​{1}}除n是否应该更快。我推理的缺陷在哪里?

3 个答案:

答案 0 :(得分:8)

第二种解决方案的主要优点是您只计算一次素数列表allPrimes。由于延迟评估,每个调用只计算它需要的素数,或者重用已经计算过的素数。所以昂贵的部分只计算一次,然后重新使用。

对于计算单个数字的最小除数,第一个版本确实更有效。但是,尝试运行ldpld来表示1和100000之间的所有数字,您会看到差异。

答案 1 :(得分:1)

haskell对我来说不得而知,如果没有对展位版本进行适当的测量,我只能假设声明是正确的。在这种情况下,原因可能是:

1.primes在某些数组中预先计算

  • 然后isprime?时间不贵

2.在运行(和记忆)

上计算.primes
  • 从开始将是第一个版本更快
  • 但是连续使用会使第二版更快(特别是对于更大的n)

3.primes是在运行时计算的(而不是记忆)

  • 如DeadMG所述
  • 编译器优化+ CPU缓存有时候会像记忆效果一样
  • 在这种情况下,版本2将整体更快
  • 但是如果你继续一直使用越来越大的n和小的那些
  • 到达缓存失效点后,它将比版本1
  • 更慢

4.这只是猜测

  • 您的编译器是否可以转换递归
  • 喜欢通过列表或范围进行单整数迭代
  • 循环? (无论如何,这种类型的大多数递归都可以转换为循环)
  • 可以解释所有......
  • 没有递归调用开销
  • 没有堆垃圾

[注]

  • 正如我上面写的那样,我不是haskell用户所以相应地对待这个答案

答案 2 :(得分:1)

据我所知,除法操作并不像你想象的2的除数那么昂贵,这使得allPrimes滤出的数字的一半被检查为“右移1位”就像计算操作一样简单,而第一种算法将通过整数来执行相对昂贵的真正除法。假设可能的除数是1956,它将被allPrimes过滤掉,只需几乎免费执行第一次测试(右移将返回零 - 可被2整除),同时将2^4253-1除以1956年已经毫无意义,因为它不能被2整除,并且如果真的有大数字,则需要花费很多时间,至少有一半(或者说5/6,对于除数2和3)是没用的。同样allPrimes是一个缓存列表,因此检查要包含在allPrimes中的素数的下一个整数仅使用经过验证的素数,因此即使对于实际素数,素数测试也不是非常昂贵。这种组合提供了第二种方法优势。