与Project Euler的速度比较:C vs Python vs Erlang vs Haskell

738
我已经将 问题#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

使用PyPy的Python:

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

Erlang:

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

Haskell:

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% (使用PyPy为118%)
  • Erlang: 436% (由于RichardC的贡献为135%)
  • Haskell: 1421%

我认为C有很大的优势,因为它使用长整型进行计算,而不是像其他三种语言一样使用任意长度的整数。此外,它不需要先加载运行时(其他语言需要吗?)。

问题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

57
@Jochen(和Seth)并不是因为C语言快速而出色,而是它被认为易于编写高效代码(这可能并不正确,但大多数程序似乎都能做到,所以足够正确)。正如我在我的回答中所探讨的,并且随着时间的推移我发现这是真的,对所选择语言的程序员技能和常见优化知识的了解非常重要(尤其是对于Haskell语言来说)。 - Thomas M. DuBuisson
57
刚用 Mathematica 进行了检查--它只需要0.25秒(使用C语言在这里需要6秒),而且代码非常简单:Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]。太棒了! - tsvikas
37
还有谁记得C语言和汇编之间的战争吗?“当然!你可以用C语言比汇编快10倍编写代码,但你的C代码能像汇编一样快运行吗?…”我相信也有过机器码和汇编之间的同样的战斗。 - JS.
43
可能不需要,因为汇编语言只是一组你输入的助记符,而不是原始二进制机器代码——通常它们之间有一对一的对应关系。 - Callum Rogers
16
对于 Haskell 程序,使用 -O2 优化编译选项可以提高程序大约 3 倍的速度,同时将 Integer 类型替换为 Int 类型可以进一步提高速度,总体加速比可达 12 倍至 14 倍以上。 - Will Ness
显示剩余20条评论
17个回答

853

在一个 x86_64 Core2 Duo(2.5GHz)的机器上,使用 GHC 7.0.3gcc 4.4.6Linux 2.6.29 编译 Haskell 和 C,分别使用 ghc -O2 -fllvm -fforce-recompgcc -O3 -lm 命令。

  • 你的 C 代码运行时间为 8.4 秒(可能因为使用了 -O3 标志而比你的运行快)
  • Haskell 代码运行时间为 36 秒(由于使用了 -O2 标志)
  • factorCount' 没有明确指定类型,而默认为 Integer(感谢 Daniel 纠正我的误诊!)。给定一个显式类型签名(这是标准实践),使用 Int 后,运行时间变为 11.1 秒
  • factorCount' 中,你需要不必要地调用了 fromIntegral。虽然修复后运行时间没有改变,但编译器很聪明,幸运的是。
  • 你使用了 mod,而 rem 更快且足够。这将时间改为 8.5 秒
  • factorCount' 不断应用两个永远不会改变的额外参数(numbersqrt)。使用 worker/wrapper 转换后可得:
 $ time ./so
 842161320  

 real    0m7.954s  
 user    0m7.944s  
 sys     0m0.004s  

没错,7.95秒。始终比C语言的解决方案快半秒钟。即使没有-fllvm标志,我仍然得到8.182秒,因此在这种情况下,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中,使用IntegerInt慢,但具体有多慢取决于执行的计算。幸运的是(对于64位机器而言),Int是足够的。为了可移植性,您应该重写我的代码以使用Int64Word64(C不是唯一带有long的语言)。

问题2:为什么Haskell如此缓慢?有没有编译器标志可以关闭它或者是我的实现问题?(后者很可能是因为Haskell对我来说是一个谜)

问题3:你能给我一些提示,如何优化这些实现,而不改变我确定因子的方式吗?任何优化方式:更好、更快、更符合语言的“本地”特性。

这就是我上面回答的内容。

  • 0)使用优化-O2
  • 1)尽可能使用快速(特别是:可以取消盒子装箱的)类型
  • 2)使用rem而不是mod(经常被遗忘的优化)
  • 3)工人/包装器转换(可能是最常见的优化)。

问题4:我的函数实现是否允许LCO,因此避免在调用堆栈上添加不必要的框架?

是的,这不是问题。干得好,很高兴您考虑了这一点。


26
因为rem实际上是mod运算的一个子组件(它们并不相同),所以如果你查看GHC Base库,你会发现mod测试了几个条件并相应地调整符号。(请参见Base.lhs中的modInt# - Thomas M. DuBuisson
23
另一个数据点:我写了一个快速的Haskell翻译C程序,没有看@Hyperboreus的Haskell程序。因此,它更接近标准惯用的Haskell,我唯一有意添加的优化是在阅读了这个答案后将mod替换为rem(嘿,出错了)。请参见链接获取我的时间记录,但简而言之就是“与C语言几乎相同”。 - C. A. McCann
117
尽管C语言版本在我的机器上运行得更快,但我现在对Haskell有了新的认识。 +1 - Seth Carnegie
13
对我来说这很惊讶,尽管我还没有尝试过。由于原始的 'factorCount' 是尾递归的,我本以为编译器可以发现额外的参数没有被改变,并只针对正在更改的参数进行优化尾递归(毕竟 Haskell 是一种纯语言,这应该很容易)。有人认为编译器可以做到这一点,或者我应该回去读更多理论论文? - kizzx2
23
@kizzx2:有一个GHC工单,希望将其添加进去。据我所知,这个变换可能会导致额外的闭包对象分配。这意味着在某些情况下性能会更差,但正如Johan Tibell在他的博客文章中所建议的那样(http://blog.johantibell.com/2010/09/static-argument-transformation.html),如果生成的包装可以内联,则可以避免这种情况。 - hammar
显示剩余15条评论

234

使用Erlang实现存在一些问题。在接下来的测试中,您未修改的Erlang程序执行时间为47.6秒,而C语言程序则只需要12.7秒。

(编辑说明:截至2021年Erlang/OTP 24版本,Erlang具有自动JIT编译器,旧的+native编译选项不再被支持或需要。我保留了下面的段落作为历史文档。有关export_all的评论仍然对于JIT生成良好代码的能力有一定参考价值。)

如果想运行计算密集型的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;

这段C代码并没有实现这个功能。一般情况下,对于同一份代码的不同实现进行公正比较可能会很棘手,尤其是在算法涉及数值计算时,因为你需要确保它们实际上执行的是相同的操作。一个实现中某处类型转换导致的轻微舍入误差可能会导致它执行比另一个更多的迭代,即使两者最终都得出相同的结果。

为了消除这种可能的错误源(并且摆脱每次迭代中的额外测试),我按照C代码的思路重新编写了factorCount函数:

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位算术”,因此除非编译器能够证明一些整数的范围(通常不能),否则它必须检查所有计算以查看它们是否适合一个单独的标记字或者是否必须将它们转换为堆分配的大整数。即使在实际运行时从未使用过大整数,这些检查也必须执行。另一方面,这意味着如果您突然提供比以前更大的输入,则 知道 算法永远不会因为意外的整数环绕而失败。

问题4:我的函数实现允许 LCO 并避免向调用堆栈添加不必要的帧吗?

是的,您的 Erlang 代码在最后调用优化方面是正确的。


3
我同意你的看法。出于多种原因,这个基准测试对于Erlang来说并不是很精确。 - Muzaaya Joshua

164
关于Python优化,除了使用PyPy(可以在不改变代码的情况下获得相当可观的速度提升),您还可以使用PyPy的翻译工具链编译符合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)

翻译为:

RPython(使用最新的PyPy修订版c2f583445aee)

% time ./euler12-rpython-c
842161320
./euler12-rpy-c  
10.54s user 0.00s 
system 99% 
cpu 10.540 total

Cython 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作为唯一参数,并且必须返回一个整数。您可以使用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,将CPU密集型代码编写为Cython扩展模块似乎是优化程序的一种非常简单的方法。


7
我很好奇,C语言版本能否被优化,使其至少与CPython一样快? - Display Name
10
@SargeBorsch,Cython 版本之所以如此快,是因为它被编译成高度优化的 C 代码,这意味着你确实可以从 C 中获得那种性能。 - Eli Korvigo

79

问题3:你能给我一些提示如何在不改变因子确定方式的情况下优化这些实现吗?任何优化方案:更好、更快、更符合语言的本质。

C语言实现是次优的(正如Thomas M. DuBuisson所暗示的),它使用了64位整数(即long数据类型)。稍后我会调查一下汇编列表,但基于我的猜测,已经在编译的代码中出现了一些内存访问,这使得使用64位整数显着变慢。可能是这样,也可能是生成的代码(不管是你可以将更少的64位整数放入SSE寄存器中还是将double四舍五入为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生成的代码更快。


22
非常不错!与之相比,在我的计算机上,您的C语言解决方案运行时间为2.5秒,而对Haskell代码进行类似修改(移动到Word32,添加INLINE pragma)则导致运行时间为4.8秒。也许可以做些什么(似乎不是很容易),gcc的结果确实令人印象深刻。 - Thomas M. DuBuisson
2
谢谢!也许问题应该是各种编译器编译输出的速度,而不是语言本身。然而,如果你有知识和时间(很多),手动优化并查阅英特尔手册仍然会完胜。 - Raedwulf

58

看看这个博客。在过去的一年左右时间里,他用Haskell和Python解决了几个Project Euler问题,并且通常发现Haskell更快。我认为,在这些语言之间,它更多地与您的流利程度和编码风格有关。

当涉及到Python速度时,你使用了错误的实现!试试PyPy,对于像这样的事情,你会发现它要快得多。


2
博客链接已失效。 - vonbrand

37

只是为了好玩。以下是一个更“本地”的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;
}

在主函数中将long类型转换为int类型,在使用gcc -O3 -lm编译选项时,这将始终以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加速给有兴趣的读者。


4
仅供参考:原始C版本:9.1690,thaumkid的版本:0.1060,提高了86倍。 - thanos
哇。一旦避免了类型推断,Haskell的性能表现非常好。 - Piyush Katariya
实际上并不是推理做到了这一点。它只有以下几个作用:A)帮助您调试或避免类型问题和类型类实例选择问题;B)通过一些现代语言扩展,调试和避免一些不可判定的类型问题。它还可以帮助您使程序无法组合,因此您永远无法扩大开发工作量。 - codeshot
C版本0.11s在英特尔骷髅峡谷上。 - codeshot

35

你的 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

6
据我上次查看,Haskell中的“primes”只是一个预先计算好的质数列表,没有计算过程,只需查找。因此,当然这会更快,但这并不能告诉你在Haskell中导出质数的计算速度。 - zxq9
26
@zxq9,您能告诉我在primes包源代码(https://hackage.haskell.org/package/primes-0.2.1.0/docs/src/Data-Numbers-Primes.html)中素数列表位于哪里吗? - Fraser
4
尽管来源显示质数没有预先计算,但这种加速非常疯狂,比 C 版本快得多,到底发生了什么? - semicolon
2
@分号记忆化。在这种情况下,我认为Haskell在运行时记忆了所有的质数,因此它不需要在每次迭代中重新计算它们。 - Hauleth
5
它有1000个因数,而不是500个。 - Casper Færgemand
显示剩余3条评论

14

使用 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在这一年里有所改进。
附注:我知道这是一个老问题,并且是从谷歌搜索中发现的(我现在忘记了我当时在搜索什么……)。只是想评论有关LCO的问题并表达我对Haskell的总体感受。我想评论最佳答案,但评论不允许使用代码块。

14
问题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只是因为它的语法清晰)。无论如何,如果你发现奇怪的东西,比如阶乘函数有两个参数(其中一个参数的名称为accaccumulator等),那么现在你知道人们为什么这样做了:)


@Hyperboreus 谢谢!我也非常好奇你的下一个问题。不过,我要提醒你我的知识有限,可能无法回答你的每一个问题。为了弥补这一点,我把我的回答设为社区维基,这样大家可以更容易地补充它。 - brandizzi
关于使用range。当我用增量while循环替换range(模仿C的for循环)时,执行时间实际上翻了一倍。我猜生成器被优化得相当好。 - Hyperboreus

9

以下是有关C语言版本的更多数字和说明,显然在这些年里没有人做过。请记得给这个答案点赞,以便让大家看到并学习。

第一步:作者程序的基准测试

笔记本电脑规格:

  • CPU i3 M380(931 MHz-最大省电模式)
  • 4GB内存
  • Win7 64位操作系统
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin with gcc 4.9.3
  • 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 目前与原始程序相同(稍后会详细说明)
  • architecture 取决于编译器设置,可以是 x86 或 x64
  • compiler 是 gcc 或 vs2012

步骤二:调查、改进和再次基准测试

VS 比 gcc 快 250%。两个编译器应该给出类似的速度。显然,代码或编译器选项存在问题。让我们调查一下!

首先要注意的是整数类型。转换可能很昂贵,一致性对于更好的代码生成和优化很重要。所有整数都应该是相同类型。

现在是 intlong 的混杂,我们将改进它。使用什么类型?最快的那种。必须对它们进行基准测试!

----------
$ 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

C语言中的整数类型包括int, long, int32_t, uint32_t, int64_t, 和 uint64_t,它们都在头文件#include <stdint.h>中定义。

在C语言中有很多整数类型可供选择,此外还有带符号/无符号和x86或x64(不要与实际的整数大小混淆)编译选项。这是很多版本需要编译和运行的^^

第三步:了解数字

结论如下:

  • 32位整数比64位整数快大约200%
  • 无符号的64位整数比有符号的64位整数快25%(不幸的是,我没有对此进行说明)

恶作剧问题:“C语言中int和long的大小是多少?”
正确答案是:C语言中的int和long的大小没有明确定义!

从C规范中看到:

int至少为32位
long至少为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个字节,也称为有符号
long,4个字节,也称为long int和signed long int

总结:所学到的经验教训

  • 32位整数比64位整数更快。

  • C语言和C++标准中的整数类型没有明确定义,它们取决于编译器和架构。当您需要一致性和可预测性时,请使用来自#include <stdint.h>uint32_t整数系列。

  • 速度问题解决了。所有其他语言都比C和C ++慢几百倍!再次赢得了胜利。下一个改进将使用OpenMP进行多线程处理:D


出于好奇,英特尔编译器的表现如何?它们通常非常擅长优化数值代码。 - kirbyfan64sos
1
你在哪里找到一份参考资料表明C语言规范保证“int至少为32位”?我所知道的唯一保证是INT_MININT_MAX的最小量级(-32767和32767,这实际上要求int至少为16位)。long需要至少与int相同大小,并且范围要求意味着long至少有32位。 - ShadowRanger
你似乎是正确的。https://dev59.com/43M_5IYBdhLWcg3wyWSt - user5994461

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接