我将 Project Euler 中的 Problem #12 作为编程练习,并比较了我在 C、Python、Erlang 和 Haskell 中的(肯定不是最佳的)实现。为了获得更高的执行时间,我搜索了第一个具有超过 1000 个除数的三角形数,而不是原始问题中所述的 500 个。
结果如下:
C:
lorenzo@enzo:~/erlang$ gcc -lm -o euler12.bin euler12.c
lorenzo@enzo:~/erlang$ time ./euler12.bin
842161320
real 0m11.074s
user 0m11.070s
sys 0m0.000s
Python:
lorenzo@enzo:~/erlang$ time ./euler12.py
842161320
real 1m16.632s
user 1m16.370s
sys 0m0.250s
Python 与 PyPy:
lorenzo@enzo:~/Downloads/pypy-c-jit-43780-b590cf6de419-linux64/bin$ time ./pypy /home/lorenzo/erlang/euler12.py
842161320
real 0m13.082s
user 0m13.050s
sys 0m0.020s
二郎:
lorenzo@enzo:~/erlang$ erlc euler12.erl
lorenzo@enzo:~/erlang$ time erl -s euler12 solve
Erlang R13B03 (erts-5.7.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.7.4 (abort with ^G)
1> 842161320
real 0m48.259s
user 0m48.070s
sys 0m0.020s
哈斯克尔:
lorenzo@enzo:~/erlang$ ghc euler12.hs -o euler12.hsx
[1 of 1] Compiling Main ( euler12.hs, euler12.o )
Linking euler12.hsx ...
lorenzo@enzo:~/erlang$ time ./euler12.hsx
842161320
real 2m37.326s
user 2m37.240s
sys 0m0.080s
概括:
C: 100%
Python:692%(118% 使用 PyPy)
Erlang:436%(135% 感谢 RichardC)
哈斯克尔:1421%
我认为 C 有一个很大的优势,因为它使用 long 进行计算,而不是像其他三个那样使用任意长度的整数。此外,它不需要先加载运行时(其他人呢?)。
问题 1: Erlang、Python 和 Haskell 是否会因为使用任意长度的整数而降低速度,或者只要值小于 MAXINT
,它们就不会降低速度吗?
问题 2:为什么 Haskell 这么慢?是否有关闭刹车的编译器标志或者是我的实现? (后者很可能是因为 Haskell 对我来说是一本有七个印章的书。)
问题 3:您能否提供一些提示,如何在不改变我确定因素的方式的情况下优化这些实现?任何方式的优化:更好、更快、更“原生”的语言。
编辑:
问题 4:我的功能实现是否允许 LCO(最后一次调用优化,也就是尾递归消除),从而避免在调用堆栈中添加不必要的帧?
我真的尝试在四种语言中实现尽可能相似的相同算法,尽管我不得不承认我的 Haskell 和 Erlang 知识非常有限。
使用的源代码:
#include <stdio.h>
#include <math.h>
int factorCount (long n)
{
double square = sqrt (n);
int isquare = (int) square;
int count = isquare == square ? -1 : 0;
long candidate;
for (candidate = 1; candidate <= isquare; candidate ++)
if (0 == n % candidate) count += 2;
return count;
}
int main ()
{
long triangle = 1;
int index = 1;
while (factorCount (triangle) < 1001)
{
index ++;
triangle += index;
}
printf ("%ld\n", triangle);
}
#! /usr/bin/env python3.2
import math
def factorCount (n):
square = math.sqrt (n)
isquare = int (square)
count = -1 if isquare == square else 0
for candidate in range (1, isquare + 1):
if not n % candidate: count += 2
return count
triangle = 1
index = 1
while factorCount (triangle) < 1001:
index += 1
triangle += index
print (triangle)
-module (euler12).
-compile (export_all).
factorCount (Number) -> factorCount (Number, math:sqrt (Number), 1, 0).
factorCount (_, Sqrt, Candidate, Count) when Candidate > Sqrt -> Count;
factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;
factorCount (Number, Sqrt, Candidate, Count) ->
case Number rem Candidate of
0 -> factorCount (Number, Sqrt, Candidate + 1, Count + 2);
_ -> factorCount (Number, Sqrt, Candidate + 1, Count)
end.
nextTriangle (Index, Triangle) ->
Count = factorCount (Triangle),
if
Count > 1000 -> Triangle;
true -> nextTriangle (Index + 1, Triangle + Index + 1)
end.
solve () ->
io:format ("~p~n", [nextTriangle (1, 1) ] ),
halt (0).
factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
where square = sqrt $ fromIntegral number
isquare = floor square
factorCount' number sqrt candidate count
| fromIntegral candidate > sqrt = count
| number `mod` candidate == 0 = factorCount' number sqrt (candidate + 1) (count + 2)
| otherwise = factorCount' number sqrt (candidate + 1) count
nextTriangle index triangle
| factorCount triangle > 1000 = triangle
| otherwise = nextTriangle (index + 1) (triangle + index + 1)
main = print $ nextTriangle 1 1
Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]
。欢呼!
在 x86_64 Core2 Duo (2.5GHz) 机器上使用 GHC 7.0.3
、gcc 4.4.6
、Linux 2.6.29
,使用 ghc -O2 -fllvm -fforce-recomp
编译 Haskell,使用 gcc -O3 -lm
编译 C。
您的 C 例程在 8.4 秒内运行(比您的运行快可能是因为 -O3)
Haskell 解决方案在 36 秒内运行(由于 -O2 标志)
您的 factorCount' 代码没有明确输入并且默认为 Integer(感谢 Daniel 在这里纠正了我的误诊!)。使用 Int 给出显式类型签名(无论如何这是标准做法),时间更改为 11.1 秒
在 factorCount' 中,您不必要地调用了 fromIntegral。修复不会导致任何变化(编译器很聪明,你很幸运)。
您使用了 rem 更快且足够的 mod。这会将时间更改为 8.5 秒。
factorCount' 不断地应用两个永远不会改变的额外参数(数字,sqrt)。工人/包装器转换为我们提供:
$ time ./so
842161320
real 0m7.954s
user 0m7.944s
sys 0m0.004s
没错,7.95 秒。始终比 C 解决方案快半秒。如果没有 -fllvm
标志,我仍然会得到 8.182 seconds
,因此 NCG 后端在这种情况下也表现良好。
结论:Haskell 很棒。
结果代码
factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
where square = sqrt $ fromIntegral number
isquare = floor square
factorCount' :: Int -> Int -> Int -> Int -> Int
factorCount' number sqrt candidate0 count0 = go candidate0 count0
where
go candidate count
| candidate > sqrt = count
| number `rem` candidate == 0 = go (candidate + 1) (count + 2)
| otherwise = go (candidate + 1) count
nextTriangle index triangle
| factorCount triangle > 1000 = triangle
| otherwise = nextTriangle (index + 1) (triangle + index + 1)
main = print $ nextTriangle 1 1
编辑:既然我们已经探索过了,让我们来解决问题
问题 1: erlang、python 和 haskell 是否会因为使用任意长度的整数而降低速度,或者只要值小于 MAXINT 就不会?
在 Haskell 中,使用 Integer
比 Int
慢,但慢多少取决于执行的计算。幸运的是(对于 64 位机器)Int
就足够了。出于可移植性考虑,您可能应该重写我的代码以使用 Int64
或 Word64
(C 不是唯一具有 long
的语言)。
问题2:为什么haskell这么慢?是否有关闭刹车的编译器标志或者是我的实现? (后者很可能是因为 haskell 对我来说是一本有七个印章的书。) 问题 3:你能给我一些提示,如何在不改变我确定因素的方式的情况下优化这些实现吗?任何方式的优化:更好、更快、更“原生”的语言。
这就是我上面回答的。答案是
0) 通过 -O2 使用优化
1) 尽可能使用快速(特别是:unbox-able)类型
2)rem not mod(一个经常被遗忘的优化)和
3)worker/wrapper转换(可能是最常见的优化)。
问题 4:我的功能实现是否允许 LCO 并因此避免在调用堆栈中添加不必要的帧?
是的,这不是问题。干得好,很高兴你考虑到了这一点。
Erlang 实现存在一些问题。作为以下基准,我测量的未修改 Erlang 程序的执行时间为 47.6 秒,而 C 代码为 12.7 秒。
如果你想运行计算密集型 Erlang 代码,你应该做的第一件事就是使用本机代码。使用 erlc +native euler12
编译将时间缩短到 41.3 秒。然而,这比在此类代码上进行本机编译的预期速度要低得多(仅 15%),问题在于您使用了 -compile(export_all)
。这对于实验很有用,但是所有函数都可能从外部访问的事实导致本机编译器非常保守。 (普通的 BEAM 仿真器不会受到太大影响。)用 -export([solve/0]).
替换这个声明可以提供更好的加速:31.5 秒(几乎是基线的 35%)。
但是代码本身有一个问题:对于 factorCount 循环中的每次迭代,您都执行以下测试:
factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;
代码不这样做。通常,在相同代码的不同实现之间进行公平比较可能很棘手,特别是如果算法是数字的,因为您需要确保它们实际上在做同样的事情。由于某处的某种类型转换,一个实现中的轻微舍入错误可能会导致它比另一个实现更多的迭代,即使两者最终达到相同的结果。
为了消除这个可能的错误源(并摆脱每次迭代中的额外测试),我重写了 factorCount 函数,如下所示,紧密地模仿 C 代码:
factorCount (N) ->
Sqrt = math:sqrt (N),
ISqrt = trunc(Sqrt),
if ISqrt == Sqrt -> factorCount (N, ISqrt, 1, -1);
true -> factorCount (N, ISqrt, 1, 0)
end.
factorCount (_N, ISqrt, Candidate, Count) when Candidate > ISqrt -> Count;
factorCount ( N, ISqrt, Candidate, Count) ->
case N rem Candidate of
0 -> factorCount (N, ISqrt, Candidate + 1, Count + 2);
_ -> factorCount (N, ISqrt, Candidate + 1, Count)
end.
这种重写,没有 export_all
,和本机编译,给了我以下运行时间:
$ erlc +native euler12.erl
$ time erl -noshell -s euler12 solve
842161320
real 0m19.468s
user 0m19.450s
sys 0m0.010s
与 C 代码相比,这还不错:
$ time ./a.out
842161320
real 0m12.755s
user 0m12.730s
sys 0m0.020s
考虑到 Erlang 根本不适合编写数字代码,在这样的程序上只比 C 慢 50% 是相当不错的。
最后,关于您的问题:
问题 1: erlang、python 和 haskell 是否会因为使用任意长度的整数而降低速度,或者只要值小于 MAXINT,它们就不会降低速度吗?
是的,有点。在 Erlang 中,没有办法说“使用带有环绕的 32/64 位算术”,因此除非编译器可以证明整数的某些界限(通常不能),否则它必须检查所有计算以查看如果它们可以放入单个标记词中,或者是否必须将它们变成堆分配的 bignums。即使在运行时实际上没有使用过 bignum,也必须执行这些检查。另一方面,这意味着你知道如果你突然给它比以前更大的输入,算法永远不会因为意外的整数环绕而失败。
问题 4:我的功能实现是否允许 LCO 并因此避免在调用堆栈中添加不必要的帧?
是的,您的 Erlang 代码在上次调用优化方面是正确的。
关于 Python 优化,除了使用 PyPy(对于代码零更改的令人印象深刻的加速)之外,您还可以使用 PyPy 的 translation toolchain 编译符合 RPython 的版本,或使用 Cython 构建扩展模块,在我的测试中,这两个版本都比 C 版本快,而 Cython 模块的速度几乎快了一倍。作为参考,我还包括 C 和 PyPy 基准测试结果:
C(用 gcc -O3 -lm
编译)
% time ./euler12-c
842161320
./euler12-c 11.95s
user 0.00s
system 99%
cpu 11.959 total
PyPy 1.5
% time pypy euler12.py
842161320
pypy euler12.py
16.44s user
0.01s system
99% cpu 16.449 total
RPython(使用最新的 PyPy 修订版,c2f583445aee
)
% time ./euler12-rpython-c
842161320
./euler12-rpy-c
10.54s user 0.00s
system 99%
cpu 10.540 total
赛通 0.15
% time python euler12-cython.py
842161320
python euler12-cython.py
6.27s user 0.00s
system 99%
cpu 6.274 total
RPython 版本有几个关键变化。要转换为独立程序,您需要定义 target
,在本例中为 main
函数。它应该接受 sys.argv
作为它的唯一参数,并且需要返回一个 int。您可以使用 translate.py,% translate.py euler12-rpython.py
将其翻译为 C 并为您编译。
# euler12-rpython.py
import math, sys
def factorCount(n):
square = math.sqrt(n)
isquare = int(square)
count = -1 if isquare == square else 0
for candidate in xrange(1, isquare + 1):
if not n % candidate: count += 2
return count
def main(argv):
triangle = 1
index = 1
while factorCount(triangle) < 1001:
index += 1
triangle += index
print triangle
return 0
if __name__ == '__main__':
main(sys.argv)
def target(*args):
return main, None
Cython 版本被重写为扩展模块 _euler12.pyx
,我从普通的 python 文件导入和调用它。 _euler12.pyx
与您的版本基本相同,但有一些额外的静态类型声明。 setup.py 具有使用 python setup.py build_ext --inplace
构建扩展的常规样板。
# _euler12.pyx
from libc.math cimport sqrt
cdef int factorCount(int n):
cdef int candidate, isquare, count
cdef double square
square = sqrt(n)
isquare = int(square)
count = -1 if isquare == square else 0
for candidate in range(1, isquare + 1):
if not n % candidate: count += 2
return count
cpdef main():
cdef int triangle = 1, index = 1
while factorCount(triangle) < 1001:
index += 1
triangle += index
print triangle
# euler12-cython.py
import _euler12
_euler12.main()
# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [Extension("_euler12", ["_euler12.pyx"])]
setup(
name = 'Euler12-Cython',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)
老实说,我对 RPython 或 Cython 的经验都很少,并且对结果感到惊喜。如果您使用 CPython,在 Cython 扩展模块中编写 CPU 密集型代码似乎是优化程序的一种非常简单的方法。
问题 3:您能否提供一些提示,如何在不改变我确定因素的方式的情况下优化这些实现?任何方式的优化:更好、更快、更“原生”的语言。
实现是次优的(正如 Thomas M. DuBuisson 所暗示的),该版本使用 64 位整数(即 long 数据类型)。稍后我将研究汇编列表,但根据有根据的猜测,编译代码中正在进行一些内存访问,这使得使用 64 位整数显着变慢。就是这样或生成的代码(事实上,您可以在 SSE 寄存器中容纳更少的 64 位整数,或者将双精度数舍入为 64 位整数会更慢)。
这是修改后的代码(只需将 long 替换为 int 并显式内联 factorCount,尽管我认为 gcc -O3 不需要这样做):
#include <stdio.h>
#include <math.h>
static inline int factorCount(int n)
{
double square = sqrt (n);
int isquare = (int)square;
int count = isquare == square ? -1 : 0;
int candidate;
for (candidate = 1; candidate <= isquare; candidate ++)
if (0 == n % candidate) count += 2;
return count;
}
int main ()
{
int triangle = 1;
int index = 1;
while (factorCount (triangle) < 1001)
{
index++;
triangle += index;
}
printf ("%d\n", triangle);
}
运行+计时它给出:
$ gcc -O3 -lm -o euler12 euler12.c; time ./euler12
842161320
./euler12 2.95s user 0.00s system 99% cpu 2.956 total
作为参考,Thomas 在较早的答案中的 haskell 实现给出了:
$ ghc -O2 -fllvm -fforce-recomp euler12.hs; time ./euler12 [9:40]
[1 of 1] Compiling Main ( euler12.hs, euler12.o )
Linking euler12 ...
842161320
./euler12 9.43s user 0.13s system 99% cpu 9.602 total
结论: ghc 没有任何好处,它是一个很棒的编译器,但 gcc 通常会生成更快的代码。
2.5 seconds
中运行,而对 Haskell 代码的类似修改(移动到 Word32,添加 INLINE pragma)导致运行时为 4.8 seconds
。也许可以做点什么(似乎不是微不足道的)——gcc 结果确实令人印象深刻。
使用 Haskell 包中的一些函数可以大大加快您的 Haskell 实现速度。在这种情况下,我使用了 primes,它只是通过 'cabal install primes' 安装的;)
import Data.Numbers.Primes
import Data.List
triangleNumbers = scanl1 (+) [1..]
nDivisors n = product $ map ((+1) . length) (group (primeFactors n))
answer = head $ filter ((> 500) . nDivisors) triangleNumbers
main :: IO ()
main = putStrLn $ "First triangle number to have over 500 divisors: " ++ (show answer)
时间:
您的原始程序:
PS> measure-command { bin\012_slow.exe }
TotalSeconds : 16.3807409
TotalMilliseconds : 16380.7409
改进的实施
PS> measure-command { bin\012.exe }
TotalSeconds : 0.0383436
TotalMilliseconds : 38.3436
如您所见,这台机器在您运行 16 秒的同一台机器上运行了 38 毫秒 :)
编译命令:
ghc -O2 012.hs -o bin\012.exe
ghc -O2 012_slow.hs -o bin\012_slow.exe
map
迭代。 Haskell,即使没有循环,仍然允许迭代,但表示为递归调用。在这个解决方案中,我们调用了 primeFactors
,它可能在下面调用了类似 primes
的东西。这个函数可能会返回可以记忆的素数列表,所以下一次调用将只计算缺失的素数顶部,而不是像 C 中的代码那样从下往上计算。
纯娱乐。以下是更“原生”的 Haskell 实现:
import Control.Applicative
import Control.Monad
import Data.Either
import Math.NumberTheory.Powers.Squares
isInt :: RealFrac c => c -> Bool
isInt = (==) <$> id <*> fromInteger . round
intSqrt :: (Integral a) => a -> Int
--intSqrt = fromIntegral . floor . sqrt . fromIntegral
intSqrt = fromIntegral . integerSquareRoot'
factorize :: Int -> [Int]
factorize 1 = []
factorize n = first : factorize (quot n first)
where first = (!! 0) $ [a | a <- [2..intSqrt n], rem n a == 0] ++ [n]
factorize2 :: Int -> [(Int,Int)]
factorize2 = foldl (\ls@((val,freq):xs) y -> if val == y then (val,freq+1):xs else (y,1):ls) [(0,0)] . factorize
numDivisors :: Int -> Int
numDivisors = foldl (\acc (_,y) -> acc * (y+1)) 1 <$> factorize2
nextTriangleNumber :: (Int,Int) -> (Int,Int)
nextTriangleNumber (n,acc) = (n+1,acc+n+1)
forward :: Int -> (Int, Int) -> Either (Int, Int) (Int, Int)
forward k val@(n,acc) = if numDivisors acc > k then Left val else Right (nextTriangleNumber val)
problem12 :: Int -> (Int, Int)
problem12 n = (!!0) . lefts . scanl (>>=) (forward n (1,1)) . repeat . forward $ n
main = do
let (n,val) = problem12 1000
print val
使用 ghc -O3
,这在我的机器(1.73GHz Core i7)上持续运行 0.55-0.58 秒。
C 版本的更有效的 factorCount 函数:
int factorCount (int n)
{
int count = 1;
int candidate,tmpCount;
while (n % 2 == 0) {
count++;
n /= 2;
}
for (candidate = 3; candidate < n && candidate * candidate < n; candidate += 2)
if (n % candidate == 0) {
tmpCount = 1;
do {
tmpCount++;
n /= candidate;
} while (n % candidate == 0);
count*=tmpCount;
}
if (n > 1)
count *= 2;
return count;
}
使用 gcc -O3 -lm
在 main 中将 long 更改为 int,这始终在 0.31-0.35 秒内运行。
如果您利用第 n 个三角形数 = n*(n+1)/2,并且 n 和 (n+1) 具有完全不同的素数分解,那么两者都可以运行得更快,因此因子的数量每一半的乘积可以求出整体的因子数。以下:
int main ()
{
int triangle = 0,count1,count2 = 1;
do {
count1 = count2;
count2 = ++triangle % 2 == 0 ? factorCount(triangle+1) : factorCount((triangle+1)/2);
} while (count1*count2 < 1001);
printf ("%lld\n", ((long long)triangle)*(triangle+1)/2);
}
将 c 代码运行时间减少到 0.17-0.19 秒,并且它可以处理更大的搜索——超过 10000 个因子在我的机器上大约需要 43 秒。我给感兴趣的读者留下了类似的haskell加速。
问题 1: erlang、python 和 haskell 是否会因为使用任意长度的整数而降低速度,或者只要值小于 MAXINT,它们就不会降低速度吗?
这是不太可能的。关于 Erlang 和 Haskell,我不能说太多(好吧,下面可能会介绍一下 Haskell),但我可以指出 Python 中的许多其他瓶颈。每次程序尝试使用 Python 中的某些值执行操作时,它都应该验证这些值是否来自正确的类型,并且会花费一些时间。您的 factorCount
函数只是分配一个具有 range (1, isquare + 1)
不同时间的列表,并且运行时,malloc
风格的内存分配比在 C 中使用计数器在范围上迭代要慢得多。值得注意的是,factorCount()
是多次调用,因此分配了很多列表。另外,我们不要忘记 Python 是解释型的,而 CPython 解释器并没有特别关注优化。
编辑:哦,好吧,我注意到您使用的是 Python 3,因此 range()
不返回列表,而是返回生成器。在这种情况下,我关于分配列表的观点有一半是错误的:该函数只分配 range
对象,尽管如此效率低下,但不如分配具有大量项目的列表效率低。
问题2:为什么haskell这么慢?是否有关闭刹车的编译器标志或者是我的实现? (后者很可能是因为 haskell 对我来说是一本有七个印章的书。)
您在使用 Hugs 吗? Hugs 是一个相当慢的解释器。如果您正在使用它,也许您可以通过 GHC 获得更好的时间 - 但我只是在思考假设,一个好的 Haskell 编译器在后台所做的事情非常令人着迷并且超出了我的理解:)
问题 3:您能否提供一些提示,如何在不改变我确定因素的方式的情况下优化这些实现?任何方式的优化:更好、更快、更“原生”的语言。
我会说你在玩一个无趣的游戏。了解各种语言的最好部分是尽可能以最不同的方式使用它们:) 但我离题了,我对此没有任何建议。对不起,我希望有人能在这种情况下帮助你:)
问题 4:我的功能实现是否允许 LCO 并因此避免在调用堆栈中添加不必要的帧?
据我记得,您只需要确保递归调用是返回值之前的最后一个命令。换句话说,像下面这样的函数可以使用这样的优化:
def factorial(n, acc=1):
if n > 1:
acc = acc * n
n = n - 1
return factorial(n, acc)
else:
return acc
但是,如果您的函数如下所示,则不会进行此类优化,因为在递归调用之后有一个操作(乘法):
def factorial2(n):
if n > 1:
f = factorial2(n-1)
return f*n
else:
return 1
我将一些局部变量中的操作分开,以明确执行哪些操作。但是,最常见的是如下所示查看这些功能,但它们与我要表达的观点是等价的:
def factorial(n, acc=1):
if n > 1:
return factorial(n-1, acc*n)
else:
return acc
def factorial2(n):
if n > 1:
return n*factorial(n-1)
else:
return 1
请注意,由编译器/解释器决定是否进行尾递归。例如,如果我记得很清楚,Python 解释器就不会这样做(我在示例中使用 Python 只是因为它的流畅语法)。无论如何,如果您发现奇怪的东西,例如带有两个参数的阶乘函数(其中一个参数的名称如 acc
、accumulator
等),那么您现在就知道人们为什么这样做了 :)
使用 Haskell,您真的不需要明确地考虑递归。
factorCount number = foldr factorCount' 0 [1..isquare] -
(fromEnum $ square == fromIntegral isquare)
where
square = sqrt $ fromIntegral number
isquare = floor square
factorCount' candidate
| number `rem` candidate == 0 = (2 +)
| otherwise = id
triangles :: [Int]
triangles = scanl1 (+) [1,2..]
main = print . head $ dropWhile ((< 1001) . factorCount) triangles
在上面的代码中,我用常见的列表操作替换了@Thomas 答案中的显式递归。代码仍然做同样的事情,而不用担心尾递归。在我的 GHC 7.6.2 机器上,它的运行速度(~7.49s)比@Thomas 回答中的版本(~7.04s)慢了大约 6%,而来自 @Raedwulf 的 C 版本运行速度约为 3.15s。似乎 GHC 在这一年里有所改善。
PS。我知道这是一个老问题,我从谷歌搜索中偶然发现了它(我忘记了我在搜索什么,现在......)。只是想评论一下关于 LCO 的问题,并表达我对 Haskell 的总体感受。我想评论最佳答案,但评论不允许代码块。
查看您的 Erlang 实现。时间包括整个虚拟机的启动、运行程序和停止虚拟机。我很确定设置和停止 erlang vm 需要一些时间。
如果计时是在 erlang 虚拟机本身内完成的,结果会有所不同,因为在这种情况下,我们将只有相关程序的实际时间。否则,我相信启动和加载 Erlang Vm 的过程所花费的总时间加上停止它所花费的总时间(正如你把它放在你的程序中一样)都包括在你用来计时的方法的总时间中程序正在输出。考虑使用 erlang 计时本身,当我们想要在虚拟机本身内对程序计时时使用它 timer:tc/1 or timer:tc/2 or timer:tc/3
。这样,erlang 的结果将排除启动和停止/杀死/停止虚拟机所花费的时间。这就是我的推理,考虑一下,然后再次尝试你的基准。
我实际上建议我们尝试在这些语言的运行时内对程序(对于具有运行时的语言)进行计时,以获得精确的值。例如,C 没有像 Erlang、Python 和 Haskell 那样启动和关闭运行时系统的开销(98% 的肯定——我支持修正)。所以(基于这个推理)我总结说这个基准对于运行在运行时系统之上的语言来说不够精确/公平。让我们用这些改变再做一次。
编辑:即使所有语言都有运行时系统,启动和停止它的开销也会有所不同。所以我建议我们从运行时系统中计时(对于这适用的语言)。众所周知,Erlang VM 在启动时有相当大的开销!
C 版本的更多数字和解释。显然,这些年来没有人这样做。记得给这个答案投票,这样它就可以让每个人都能看到和学习。
第一步:作者程序的基准测试
笔记本电脑规格:
CPU i3 M380(931 MHz - 最大省电模式)
4GB内存
Win7 64位
微软 Visual Studio 2012 终极版
带有 gcc 4.9.3 的 Cygwin
Python 2.7.10
命令:
compiling on VS x64 command prompt > `for /f %f in ('dir /b *.c') do cl /O2 /Ot /Ox %f -o %f_x64_vs2012.exe`
compiling on cygwin with gcc x64 > `for f in ./*.c; do gcc -m64 -O3 $f -o ${f}_x64_gcc.exe ; done`
time (unix tools) using cygwin > `for f in ./*.exe; do echo "----------"; echo $f ; time $f ; done`
.
----------
$ time python ./original.py
real 2m17.748s
user 2m15.783s
sys 0m0.093s
----------
$ time ./original_x86_vs2012.exe
real 0m8.377s
user 0m0.015s
sys 0m0.000s
----------
$ time ./original_x64_vs2012.exe
real 0m8.408s
user 0m0.000s
sys 0m0.015s
----------
$ time ./original_x64_gcc.exe
real 0m20.951s
user 0m20.732s
sys 0m0.030s
文件名是:integertype_architecture_compiler.exe
integertype 现在与原始程序相同(稍后会详细介绍)
架构是 x86 或 x64,具体取决于编译器设置
编译器是 gcc 或 vs2012
第二步:再次调查、改进和基准测试
VS 比 gcc 快 250%。这两个编译器应该给出相似的速度。显然,代码或编译器选项有问题。让我们调查一下!
第一个兴趣点是整数类型。转换可能很昂贵,并且一致性对于更好的代码生成和优化很重要。所有整数都应该是同一类型。
现在是 int
和 long
的混杂。我们将对此进行改进。使用什么类型?最快的。必须对它们进行基准测试!
----------
$ time ./int_x86_vs2012.exe
real 0m8.440s
user 0m0.016s
sys 0m0.015s
----------
$ time ./int_x64_vs2012.exe
real 0m8.408s
user 0m0.016s
sys 0m0.015s
----------
$ time ./int32_x86_vs2012.exe
real 0m8.408s
user 0m0.000s
sys 0m0.015s
----------
$ time ./int32_x64_vs2012.exe
real 0m8.362s
user 0m0.000s
sys 0m0.015s
----------
$ time ./int64_x86_vs2012.exe
real 0m18.112s
user 0m0.000s
sys 0m0.015s
----------
$ time ./int64_x64_vs2012.exe
real 0m18.611s
user 0m0.000s
sys 0m0.015s
----------
$ time ./long_x86_vs2012.exe
real 0m8.393s
user 0m0.015s
sys 0m0.000s
----------
$ time ./long_x64_vs2012.exe
real 0m8.440s
user 0m0.000s
sys 0m0.015s
----------
$ time ./uint32_x86_vs2012.exe
real 0m8.362s
user 0m0.000s
sys 0m0.015s
----------
$ time ./uint32_x64_vs2012.exe
real 0m8.393s
user 0m0.015s
sys 0m0.015s
----------
$ time ./uint64_x86_vs2012.exe
real 0m15.428s
user 0m0.000s
sys 0m0.015s
----------
$ time ./uint64_x64_vs2012.exe
real 0m15.725s
user 0m0.015s
sys 0m0.015s
----------
$ time ./int_x64_gcc.exe
real 0m8.531s
user 0m8.329s
sys 0m0.015s
----------
$ time ./int32_x64_gcc.exe
real 0m8.471s
user 0m8.345s
sys 0m0.000s
----------
$ time ./int64_x64_gcc.exe
real 0m20.264s
user 0m20.186s
sys 0m0.015s
----------
$ time ./long_x64_gcc.exe
real 0m20.935s
user 0m20.809s
sys 0m0.015s
----------
$ time ./uint32_x64_gcc.exe
real 0m8.393s
user 0m8.346s
sys 0m0.015s
----------
$ time ./uint64_x64_gcc.exe
real 0m16.973s
user 0m16.879s
sys 0m0.030s
整数类型是来自 #include <stdint.h>
的 int
long
int32_t
uint32_t
int64_t
和 uint64_t
C 中有很多整数类型,还有一些有符号/无符号可供使用,以及编译为 x86 或 x64 的选择(不要与实际的整数大小混淆)。有很多版本要编译和运行^^
第三步:理解数字
明确结论:
32 位整数比 64 位整数快约 200%
无符号 64 位整数比有符号 64 位快 25%(不幸的是,我对此没有任何解释)
技巧问题:“C 中 int 和 long 的大小是多少?”正确答案是:C 中 int 和 long 的大小没有明确定义!
从 C 规范:
int 至少 32 位长 至少是一个 int
从 gcc 手册页(-m32 和 -m64 标志):
32 位环境将 int、long 和指针设置为 32 位,并生成可在任何 i386 系统上运行的代码。 64 位环境将 int 设置为 32 位,将 long 和指针设置为 64 位,并为 AMD 的 x86-64 架构生成代码。
来自 MSDN 文档(数据类型范围)https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx:
int,4 字节,也称为signed long,4 字节,也称为long int 和signed long int
总结这一点:经验教训
32 位整数比 64 位整数快。
标准整数类型在 C 或 C++ 中没有得到很好的定义,它们因编译器和体系结构而异。当您需要一致性和可预测性时,请使用 #include
速度问题解决了。所有其他语言都落后了百分百,C 和 C++ 再次获胜!他们总是这样做。下一个改进将是使用 OpenMP 的多线程:D
INT_MIN
和 INT_MAX
的最小幅度(-32767 和 32767,实际上要求 int
至少为 16 位)。 long
要求至少与 int
一样大,并且范围要求意味着 long
至少为 32 位。
问题 1:Erlang、Python 和 Haskell 是否会因为使用任意长度的整数而降低速度,或者只要值小于 MAXINT,它们就不会降低速度吗?
对于 Erlang,问题一的答案是否定的。最后一个问题通过适当地使用 Erlang 来回答,如下所示:
http://bredsaal.dk/learning-erlang-using-projecteuler-net
由于它比您最初的 C 示例更快,我猜想有很多问题,因为其他人已经详细介绍过。
这个 Erlang 模块在便宜的上网本上执行大约需要 5 秒……它使用 erlang 中的网络线程模型,因此演示了如何利用事件模型。它可以分布在许多节点上。而且速度很快。不是我的代码。
-module(p12dist).
-author("Jannich Brendle, jannich@bredsaal.dk, http://blog.bredsaal.dk").
-compile(export_all).
server() ->
server(1).
server(Number) ->
receive {getwork, Worker_PID} -> Worker_PID ! {work,Number,Number+100},
server(Number+101);
{result,T} -> io:format("The result is: \~w.\~n", [T]);
_ -> server(Number)
end.
worker(Server_PID) ->
Server_PID ! {getwork, self()},
receive {work,Start,End} -> solve(Start,End,Server_PID)
end,
worker(Server_PID).
start() ->
Server_PID = spawn(p12dist, server, []),
spawn(p12dist, worker, [Server_PID]),
spawn(p12dist, worker, [Server_PID]),
spawn(p12dist, worker, [Server_PID]),
spawn(p12dist, worker, [Server_PID]).
solve(N,End,_) when N =:= End -> no_solution;
solve(N,End,Server_PID) ->
T=round(N*(N+1)/2),
case (divisor(T,round(math:sqrt(T))) > 500) of
true ->
Server_PID ! {result,T};
false ->
solve(N+1,End,Server_PID)
end.
divisors(N) ->
divisor(N,round(math:sqrt(N))).
divisor(_,0) -> 1;
divisor(N,I) ->
case (N rem I) =:= 0 of
true ->
2+divisor(N,I-1);
false ->
divisor(N,I-1)
end.
以下测试在以下设备上进行:Intel(R) Atom(TM) CPU N270 @ 1.60GHz
~$ time erl -noshell -s p12dist start
The result is: 76576500.
^C
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
a
real 0m5.510s
user 0m5.836s
sys 0m0.152s
尝试去:
package main
import "fmt"
import "math"
func main() {
var n, m, c int
for i := 1; ; i++ {
n, m, c = i * (i + 1) / 2, int(math.Sqrt(float64(n))), 0
for f := 1; f < m; f++ {
if n % f == 0 { c++ }
}
c *= 2
if m * m == n { c ++ }
if c > 1001 {
fmt.Println(n)
break
}
}
}
我得到:
原始 c 版本:9.1690 100% 去:8.2520 111%
但是使用:
package main
import (
"math"
"fmt"
)
// Sieve of Eratosthenes
func PrimesBelow(limit int) []int {
switch {
case limit < 2:
return []int{}
case limit == 2:
return []int{2}
}
sievebound := (limit - 1) / 2
sieve := make([]bool, sievebound+1)
crosslimit := int(math.Sqrt(float64(limit))-1) / 2
for i := 1; i <= crosslimit; i++ {
if !sieve[i] {
for j := 2 * i * (i + 1); j <= sievebound; j += 2*i + 1 {
sieve[j] = true
}
}
}
plimit := int(1.3*float64(limit)) / int(math.Log(float64(limit)))
primes := make([]int, plimit)
p := 1
primes[0] = 2
for i := 1; i <= sievebound; i++ {
if !sieve[i] {
primes[p] = 2*i + 1
p++
if p >= plimit {
break
}
}
}
last := len(primes) - 1
for i := last; i > 0; i-- {
if primes[i] != 0 {
break
}
last = i
}
return primes[0:last]
}
func main() {
fmt.Println(p12())
}
// Requires PrimesBelow from utils.go
func p12() int {
n, dn, cnt := 3, 2, 0
primearray := PrimesBelow(1000000)
for cnt <= 1001 {
n++
n1 := n
if n1%2 == 0 {
n1 /= 2
}
dn1 := 1
for i := 0; i < len(primearray); i++ {
if primearray[i]*primearray[i] > n1 {
dn1 *= 2
break
}
exponent := 1
for n1%primearray[i] == 0 {
exponent++
n1 /= primearray[i]
}
if exponent > 1 {
dn1 *= exponent
}
if n1 == 1 {
break
}
}
cnt = dn * dn1
dn = dn1
}
return n * (n - 1) / 2
}
我得到:
原始 c 版本:9.1690 100% thaumkid 的 c 版本:0.1060 8650% 第一个版本:8.2520 111% 第二个版本:0.0230 39865%
我还尝试了 Python3.6 和 pypy3.3-5.5-alpha:
原始 c 版本:8.629 100% thaumkid 的 c 版本:0.109 7916% Python3.6:54.795 16% pypy3.3-5.5-alpha:13.291 65%
然后我得到以下代码:
原始 c 版本:8.629 100% thaumkid 的 c 版本:0.109 8650% Python3.6:1.489 580% pypy3.3-5.5-alpha:0.582 1483%
def D(N):
if N == 1: return 1
sqrtN = int(N ** 0.5)
nf = 1
for d in range(2, sqrtN + 1):
if N % d == 0:
nf = nf + 1
return 2 * nf - (1 if sqrtN**2 == N else 0)
L = 1000
Dt, n = 0, 0
while Dt <= L:
t = n * (n + 1) // 2
Dt = D(n/2)*D(n+1) if n%2 == 0 else D(n)*D((n+1)/2)
n = n + 1
print (t)
C++11, <我需要 20 毫秒 - Run it here
我知道您想要帮助提高您的语言特定知识的提示,但是由于这里已经很好地介绍了这一点,我想我会为可能已经看过对您的问题等的数学评论的人添加一些上下文,并想知道为什么会这样代码慢得多。
这个答案主要是提供上下文,希望能帮助人们更轻松地评估您的问题/其他答案中的代码。
此代码仅使用了几个(丑陋的)优化,与所使用的语言无关,基于:
每个 traingle 数的形式为 n(n+1)/2 n 和 n+1 互质 除数的数量是乘法函数
#include <iostream>
#include <cmath>
#include <tuple>
#include <chrono>
using namespace std;
// Calculates the divisors of an integer by determining its prime factorisation.
int get_divisors(long long n)
{
int divisors_count = 1;
for(long long i = 2;
i <= sqrt(n);
/* empty */)
{
int divisions = 0;
while(n % i == 0)
{
n /= i;
divisions++;
}
divisors_count *= (divisions + 1);
//here, we try to iterate more efficiently by skipping
//obvious non-primes like 4, 6, etc
if(i == 2)
i++;
else
i += 2;
}
if(n != 1) //n is a prime
return divisors_count * 2;
else
return divisors_count;
}
long long euler12()
{
//n and n + 1
long long n, n_p_1;
n = 1; n_p_1 = 2;
// divisors_x will store either the divisors of x or x/2
// (the later iff x is divisible by two)
long long divisors_n = 1;
long long divisors_n_p_1 = 2;
for(;;)
{
/* This loop has been unwound, so two iterations are completed at a time
* n and n + 1 have no prime factors in common and therefore we can
* calculate their divisors separately
*/
long long total_divisors; //the divisors of the triangle number
// n(n+1)/2
//the first (unwound) iteration
divisors_n_p_1 = get_divisors(n_p_1 / 2); //here n+1 is even and we
total_divisors =
divisors_n
* divisors_n_p_1;
if(total_divisors > 1000)
break;
//move n and n+1 forward
n = n_p_1;
n_p_1 = n + 1;
//fix the divisors
divisors_n = divisors_n_p_1;
divisors_n_p_1 = get_divisors(n_p_1); //n_p_1 is now odd!
//now the second (unwound) iteration
total_divisors =
divisors_n
* divisors_n_p_1;
if(total_divisors > 1000)
break;
//move n and n+1 forward
n = n_p_1;
n_p_1 = n + 1;
//fix the divisors
divisors_n = divisors_n_p_1;
divisors_n_p_1 = get_divisors(n_p_1 / 2); //n_p_1 is now even!
}
return (n * n_p_1) / 2;
}
int main()
{
for(int i = 0; i < 1000; i++)
{
using namespace std::chrono;
auto start = high_resolution_clock::now();
auto result = euler12();
auto end = high_resolution_clock::now();
double time_elapsed = duration_cast<milliseconds>(end - start).count();
cout << result << " " << time_elapsed << '\n';
}
return 0;
}
我的台式机平均需要 19 毫秒,笔记本电脑平均需要 80 毫秒,这与我在这里看到的大多数其他代码相去甚远。毫无疑问,还有许多优化可用。
我假设只有当涉及的数字有很多小因素时,因素的数量才会很大。所以我使用了 thaumkid 的优秀算法,但首先使用了一个永远不会太小的因子计数的近似值。这很简单:检查最多 29 的质因数,然后检查剩余的数并计算因数数量的上限。使用它来计算因子数量的上限,如果该数字足够高,则计算因子的确切数量。
下面的代码不需要这个假设来保证正确性,但要快。它似乎有效;只有大约 100,000 个数字中的一个给出的估计值足够高,需要进行全面检查。
这是代码:
// Return at least the number of factors of n.
static uint64_t approxfactorcount (uint64_t n)
{
uint64_t count = 1, add;
#define CHECK(d) \
do { \
if (n % d == 0) { \
add = count; \
do { n /= d; count += add; } \
while (n % d == 0); \
} \
} while (0)
CHECK ( 2); CHECK ( 3); CHECK ( 5); CHECK ( 7); CHECK (11); CHECK (13);
CHECK (17); CHECK (19); CHECK (23); CHECK (29);
if (n == 1) return count;
if (n < 1ull * 31 * 31) return count * 2;
if (n < 1ull * 31 * 31 * 37) return count * 4;
if (n < 1ull * 31 * 31 * 37 * 37) return count * 8;
if (n < 1ull * 31 * 31 * 37 * 37 * 41) return count * 16;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43) return count * 32;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47) return count * 64;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53) return count * 128;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59) return count * 256;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61) return count * 512;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67) return count * 1024;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71) return count * 2048;
if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71 * 73) return count * 4096;
return count * 1000000;
}
// Return the number of factors of n.
static uint64_t factorcount (uint64_t n)
{
uint64_t count = 1, add;
CHECK (2); CHECK (3);
uint64_t d = 5, inc = 2;
for (; d*d <= n; d += inc, inc = (6 - inc))
CHECK (d);
if (n > 1) count *= 2; // n must be a prime number
return count;
}
// Prints triangular numbers with record numbers of factors.
static void printrecordnumbers (uint64_t limit)
{
uint64_t record = 30000;
uint64_t count1, factor1;
uint64_t count2 = 1, factor2 = 1;
for (uint64_t n = 1; n <= limit; ++n)
{
factor1 = factor2;
count1 = count2;
factor2 = n + 1; if (factor2 % 2 == 0) factor2 /= 2;
count2 = approxfactorcount (factor2);
if (count1 * count2 > record)
{
uint64_t factors = factorcount (factor1) * factorcount (factor2);
if (factors > record)
{
printf ("%lluth triangular number = %llu has %llu factors\n", n, factor1 * factor2, factors);
record = factors;
}
}
}
}
这将在大约 0.7 秒内找到具有 13824 个因数的第 14,753,024 个三角形,在 34 秒内找到具有 61,440 个因数的第 879,207,615 个三角形数,在 10 分 5 秒内找到具有 138,240 个因数的第 12,524,486,975 个三角形数,以及具有 26,467,792,063 个三角形数的第 26,467,792,063 个三角形数21 分 25 秒(2.4GHz Core2 Duo),所以这个代码平均每个数字只需要 116 个处理器周期。最后一个三角数本身大于 2^68,所以
我将“Jannich Brendle”版本修改为 1000 而不是 500。并列出了 euler12.bin、euler12.erl、p12dist.erl 的结果。两个 erl 代码都使用 '+native' 进行编译。
zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s p12dist start
The result is: 842161320.
real 0m3.879s
user 0m14.553s
sys 0m0.314s
zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s euler12 solve
842161320
real 0m10.125s
user 0m10.078s
sys 0m0.046s
zhengs-MacBook-Pro:workspace zhengzhibin$ time ./euler12.bin
842161320
real 0m5.370s
user 0m5.328s
sys 0m0.004s
zhengs-MacBook-Pro:workspace zhengzhibin$
#include <stdio.h>
#include <math.h>
int factorCount (long n)
{
double square = sqrt (n);
int isquare = (int) square+1;
long candidate = 2;
int count = 1;
while(candidate <= isquare && candidate <= n){
int c = 1;
while (n % candidate == 0) {
c++;
n /= candidate;
}
count *= c;
candidate++;
}
return count;
}
int main ()
{
long triangle = 1;
int index = 1;
while (factorCount (triangle) < 1001)
{
index ++;
triangle += index;
}
printf ("%ld\n", triangle);
}
gcc -lm -Ofast euler.c
时间./a.out
2.79s 用户 0.00s 系统 99% cpu 2.794 总
rem
实际上是mod
操作的子组件(它们不一样)。如果您查看 GHC Base 库,您会看到mod
测试几个条件并相应地调整符号。 (见Base.lhs
中的modInt#
)mod
替换为rem
(呵呵,哎呀)。请参阅链接了解我的时间安排,但简短版本“几乎与 C 相同”。factorCount'
是尾递归的,我原以为编译器可以发现没有更改的额外参数并仅针对更改的参数优化尾递归(毕竟 Haskell 是一种纯语言,这应该很容易)。有人认为编译器可以做到这一点,还是我应该回去阅读更多的理论论文?