计算素数时Haskell的效率

时间:2015-01-10 13:39:35

标签: performance haskell primes

我有以下一组函数来计算Haskell中小于或等于数n的素数。

算法取一个数字,检查它是否可被2整除,然后检查它是否可以被奇数整除,直到被检查数的平方根。

-- is a numner, n, prime? 
isPrime :: Int -> Bool
isPrime n = n > 1 &&
              foldr (\d r -> d * d > n || (n `rem` d /= 0 && r))
                True divisors

-- list of divisors for which to test primality
divisors :: [Int]
divisors = 2:[3,5..]

-- pi(n) - the prime counting function, the number of prime numbers <= n
primesNo :: Int -> Int
primesNo 2 = 1
primesNo n
    | isPrime n = 1 + primesNo (n-1)
    | otherwise = 0 + primesNo (n-1)

main = print $ primesNo (2^22)

将GHC与-O2优化标志一起使用,计算n = 2 ^ 22的素数需要~3.8秒。以下C代码需要~0.8秒:

#include <stdio.h>
#include <math.h>

/*
    compile with: gcc -std=c11 -lm -O2 c_primes.c -o c_orig
*/

int isPrime(int n) {
    if (n < 2)
        return 0;
    else if (n == 2)
        return 1;
    else if (n % 2 == 0)
        return 0;
    int uL = sqrt(n);
    int i = 3;
    while (i <= uL) {
        if (n % i == 0)
            return 0;
        i+=2;
    }
    return 1;
}

int main() {
    int noPrimes = 0, limit = 4194304;
    for (int n = 0; n <= limit; n++) {
        if (isPrime(n))
            noPrimes++;
    }
    printf("Number of primes in the interval [0,%d]: %d\n", limit, noPrimes);
    return 0;
}

这个算法在Java中占用大约0.9秒,在JavaScript中占用1.8秒(在节点上),所以它只是觉得Haskell版本比我预期的要慢。无论如何,我可以在不改变算法的情况下更有效地在Haskell中编码吗?


修改

@dfeuer提供的以下版本的isPrime将运行时间缩短了一秒,将其降低到2.8秒(从3.8降低)。虽然这仍然比JavaScript(节点)慢,这需要大约1.8秒,如此处所示,Yet Another Language Speed Test

isPrime :: Int -> Bool
isPrime n
  | n <= 2 = n == 2
  | otherwise = odd n && go 3
  where
    go factor
      | factor * factor > n = True
      | otherwise = n `rem` factor /= 0 && go (factor+2) 

修改

在上面的 isPrime 函数中,函数 go 为每个除数调用 factor * factor ñ。我认为将 factor n 的平方根进行比较会更有效,因为这只需要按 n 计算一次。但是,使用以下代码,计算时间增加了大约10%,每次评估不等式时, n 的平方根被重新计算(对于每个因子 )?

isPrime :: Int -> Bool
isPrime n
  | n <= 2 = n == 2
  | otherwise = odd n && go 3
  where
    go factor
      | factor > upperLim = True
      | otherwise = n `rem` factor /= 0 && go (factor+2)
      where
        upperLim = (floor.sqrt.fromIntegral) n 

4 个答案:

答案 0 :(得分:4)

我建议您使用其他算法,例如Melissa O'Neill在paper中讨论的Eratosthenes筛,或arithmoi包中Math.NumberTheory.Primes中使用的版本,它还提供优化的计数功能。但是,这可能会让您获得更好的常数因素:

-- is a number, n, prime? 
isPrime :: Int -> Bool
isPrime n
  | n <= 2 = n == 2
  | otherwise = odd n && -- Put the 2 here instead
        foldr (\d r -> d * d > n || (n `rem` d /= 0 && r))
                True divisors

-- list of divisors for which to test primality
divisors :: [Int]
{-# INLINE divisors #-} -- No guarantee, but it might possibly inline and stay inlined,
               -- so the numbers will be generated on each call instead of
               -- being pulled in (expensively) from RAM.
divisors = [3,5..] -- No more 2:

摆脱2:的原因是,称为“foldr / build fusion”,“short cut deforestation”或者只是“list fusion”的优化可能会使你的除数列表消失,但是,至少GHC&lt; 7.10.1,2:将阻止优化。


编辑:它似乎不适合你,所以这里还有其他的尝试:

isPrime n
  | n <= 2 = n == 2
  | otherwise = odd n && go 3
  where
    go factor
      | factor * factor > n = True
      | otherwise = n `rem` factor /= 0 && go (factor+2) 

答案 1 :(得分:3)

总的来说,我发现在Haskell中循环比用C完成的循环慢大约3-4倍。

为了帮助理解性能差异,我略微修改了 程序,以便每次迭代进行固定数量的除数测试 并添加了一个参数 e 来控制迭代次数 - 执行的(外部)迭代次数为2 ^ e。对于每个外部迭代 约。进行了2 ^ 21次除数测试。

每个程序和脚本的源代码,用于运行和分析 结果可在此处找到:https://github.com/erantapaa/loopbench

欢迎拉动请求以改进基准测试。

以下是使用ghc 7.8.3(在OSX下)在2.4 GHz Intel Core 2 Duo上获得的结果。使用的gcc是&#34; Apple LLVM版本6.0(clang-600.0.56)(基于LLVM 3.5svn)&#34;。

e     ctime   htime  allocated  gc-bytes alloc/iter  h/c      dns
10   0.0101  0.0200      87424      3408             1.980   4.61
11   0.0151  0.0345     112000      3408             2.285   4.51
12   0.0263  0.0700     161152      3408             2.661   5.09
13   0.0472  0.1345     259456      3408             2.850   5.08
14   0.0819  0.2709     456200      3408             3.308   5.50
15   0.1575  0.5382     849416      9616             3.417   5.54
16   0.3112  1.0900    1635848     15960             3.503   5.66
17   0.6105  2.1682    3208848     15984             3.552   5.66
18   1.2167  4.3536    6354576     16032  24.24      3.578   5.70
19   2.4092  8.7336   12646032     16128  24.12      3.625   5.75
20   4.8332 17.4109   25229080     16320  24.06      3.602   5.72

e          = exponent parameter
ctime      = running time of the C program
htime      = running time of the Haskell program
allocated  = bytes allocated in the heap (Haskell program)
gc-bytes   = bytes copied during GC (Haskell program)
alloc/iter = bytes allocated in the heap / 2^e
h / c      = htime divided by ctime
dns        = (htime - ctime) divided by the number of divisor tests made
             in nanoseconds

# divisor tests made = 2^e * 2^11

一些观察结果:

  1. Haskell程序以每(外部)循环迭代大约24个字节的速率执行堆分配。 C程序显然不执行任何分配并完全在L1缓存中运行。
  2. gc-bytes计数在10到14之间保持不变,因为没有为这些运行执行垃圾收集。
  3. 随着更多分配的进行,时间比率h / c逐渐变差。
  4. dps衡量Haskell程序每次除数测试所需的额外时间;它随着分配总量的增加而增加。还有一些高原表明这是由于缓存效应。
  5. 众所周知,GHC不会产生相同的紧密循环代码 一个C编译器生成。您支付的罚款约为。每次迭代4.6 ns。 而且,看起来Haskell也受到缓存效果的影响 堆分配。

    每个分配24个字节,每个循环迭代5个不是很多 大多数程序,但是当你有2 ^ 20个分配和2 ^ 40个循环迭代时 它成为一个因素。

答案 2 :(得分:2)

C代码使用32位整数,而Haskell代码使用64位整数。

原始C代码在我的计算机上以 0.63 秒运行。但是,如果我将int - s替换为long - s,则使用gcc以 2.07 秒运行,使用clang以 2.17 秒运行。< / p>

相比之下,更新的isPrime函数(在线程问题中查看)在 2.09 秒内运行(使用-O2和-fllvm)。注意,它比clang编译的C代码略好,即使它们使用相同的LLVM代码生成器。

原始的Haskell代码以 3.2 秒运行,我认为这是为了方便使用列表进行迭代而可接受的开销。

答案 3 :(得分:1)

内联所有内容,松散多余的测试,添加严格的注释以确保:

{-# LANGUAGE BangPatterns #-}

-- pi(n) - the prime counting function, the number of prime numbers <= n
primesNo :: Int -> Int
primesNo n
    | n < 2 = 0
    | otherwise = g 3 1
 where
   g  k !cnt | k > n     = cnt
             | go 3      = g  (k+2) (cnt+1)
             | otherwise = g  (k+2) cnt
      where go f 
               | f*f > k   = True
               | otherwise = k `rem` f /= 0 && go (f+2) 

main = print $ primesNo (2^22)

go测试功能与dfeuer的答案一样。像往常一样使用-O2编译,并且始终通过运行独立的可执行文件进行测试(类似> test +RTS -s)。

可以直接拨打g电话(实际上是对其进行微观优化):

primesNo n
    | n < 2 = 0
    | otherwise = g 3 1
 where
   g  k !cnt | k > n     = cnt
             | otherwise = go 3
      where go f 
               | f*f > k        = g (k+2) (cnt+1)
               | k `rem` f == 0 = g (k+2) cnt
               | otherwise      = go (f+2)

更大幅度的改变(仍然保持算法可以说是相同的)可能或不可能加速它是将其内部转出,以避免方块计算:从[3]测试所有赔率9至{23},[3,5]所有赔率从25到47等等,沿this segmented code行:

import Data.List (inits)

primesNo n = length (takeWhile (<= n) $ 2 : oddprimes)
  where
    oddprimes = sieve 3 9 [3,5..] (inits [3,5..]) 
    sieve x q ~(_:t) (fs:ft) =
      filter ((`all` fs) . ((/=0).) . rem) [x,x+2..q-2]
      ++ sieve (q+2) (head t^2) t ft

有时将代码调整为使用and代替all也会改变速度。可以通过内联和简化所有内容来尝试进一步加速(用计数等替换length)。