我有以下一组函数来计算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
答案 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
一些观察结果:
众所周知,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
)。