【问题标题】:Speed comparison with Project Euler: C vs Python vs Erlang vs Haskell与 Project Euler 的速度比较:C vs Python vs Erlang vs Haskell
【发布时间】:2011-10-21 07:40:07
【问题描述】:

我将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

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%(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

【问题讨论】:

  • @Jochen(和 Seth) 并不是说​​ C 语言很快或很棒,但它被认为很容易编写高性能代码(这可能不是真的,但大多数程序似乎都能够,所以真的足够的)。正如我在回答中所探索的那样,随着时间的推移发现这是正确的,程序员技能和所选语言的常见优化知识非常重要(尤其是对于 Haskell)。
  • 刚刚用 Mathematica 检查过——需要 0.25 秒(这里用 C 需要 6 秒),代码就是:Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] &lt; x, i++, s += i]; s] .万岁!
  • 还有其他人记得 C 和汇编之间的这些战争吗? “当然!你可以用 C 语言编写代码快 10 倍,但你的 C 代码可以运行这么快吗?...”我敢肯定机器代码和汇编之间也有同样的战斗。
  • @JS:可能不会,因为汇编只是您键入的一组助记符,而不是原始二进制机器码——通常它们之间存在 1-1 对应关系。
  • 结论,对于 Haskell:-O2 使其加速大约 3 倍,而使用 Int 而不是 Integer 大约 4x-6x,总加速达到 12x-14x 甚至更多。

标签: python c performance haskell erlang


【解决方案1】:
#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 user 0.00s system 99% cpu 2.794 total

【讨论】:

    【解决方案2】:

    尝试 GO:

    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)
    

    【讨论】:

      【解决方案3】:

      C 版本的更多数字和解释。显然,这些年来没有人这样做。记得给这个答案投票,这样它就可以让每个人都能看到和学习​​。

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

      笔记本电脑规格:

      • CPU i3 M380(931 MHz - 最大省电模式)
      • 4GB 内存
      • Win7 64 位
      • Microsoft Visual Studio 2012 Ultimate
      • 带有 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%。这两个编译器应该给出相似的速度。显然,代码或编译器选项有问题。让我们调查一下!

      第一个兴趣点是整数类型。转换可能很昂贵,并且一致性对于更好的代码生成和优化很重要。所有整数都应该是同一类型。

      现在是 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
      

      整数类型是intlongint32_tuint32_tint64_tuint64_t来自#include &lt;stdint.h&gt;

      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 &lt;stdint.h&gt;uint32_t 整数族。

      • 速度问题已解决。所有其他语言都落后了百分百,C 和 C++ 再次获胜!他们总是这样做。下一个改进将是使用 OpenMP 的多线程:D

      【讨论】:

      • 出于好奇,英特尔编译器是怎么做的?他们通常非常擅长优化数字代码。
      • 您在哪里可以找到说明 C 规范保证“int 至少为 32 位”的参考资料?我知道的唯一保证是INT_MININT_MAX 的最小幅度(-32767 和32767,实际上要求int 至少为16 位)。 long 要求至少与 int 一样大,并且范围要求意味着 long 至少为 32 位。
      【解决方案4】:

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

      【讨论】:

      • 如下将值增加到 1000 不会获得正确的结果。如上 > 500,最新测试:IntelCore2 CPU 6600 @ 2.40GHz 在实际 0m2.370s 内完成
      • 你的结果:76576500 其他人:842161320 你的结果有问题
      • 因为我在做一些其他的欧拉问题,所以我只是检查了我的结果。 projecteuler.net/problem=12 的答案是 76576500 毫无疑问。我知道这看起来很奇怪,但我只是检查了一下。
      • 为了比较,我得到 9.03 与原始 c 版本,而使用 Erlang 19 和 Mark 的代码,我得到 5.406,快 167.0366%。
      【解决方案5】:

      关于 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
      

      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 作为它的唯一参数,并且需要返回一个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 密集型代码似乎是优化程序的一种非常简单的方法。

      【讨论】:

      • 我很好奇,C 版本可以优化到至少和 CPython 一样快吗?
      • @SargeBorsch 说 Cython 版本如此之快,因为它编译为高度优化的 C 源代码,这意味着您肯定可以从 C 中获得该性能。
      【解决方案6】:

      C++11, - Run it here

      我知道您需要一些提示来帮助提高您的语言特定知识,但是由于这里已经很好地介绍了这一点,我想我会为可能已经看过有关您的问题等的数学评论的人添加一些上下文,并且想知道为什么这段代码这么慢。

      这个答案主要是提供上下文,希望能帮助人们更轻松地评估您的问题/其他答案中的代码。

      此代码仅使用了几个(丑陋的)优化,与所使用的语言无关,基于:

      1. 每个 traingle 数的形式为 n(n+1)/2
      2. n 和 n+1 互质
      3. 除数是乘法函数

      #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 毫秒,这与我在这里看到的大多数其他代码相去甚远。毫无疑问,还有许多优化可用。

      【讨论】:

      • 这显然不是提问者所要求的,“我真的试图在四种语言中实现尽可能相似的相同算法”。引用与您类似的许多已删除答案之一的评论“很明显,无论语言如何,您都可以使用更好的算法获得更快的速度。”
      • @ThomasM.DuBuisson。这就是我要说的。问题\答案严重暗示算法加速很重要(当然 OP 并没有要求它们),但没有明确的例子。我认为这个答案 - 这不是完全优化的代码 - 为像我这样想知道 OP 代码有多慢/多快的任何人提供了一些有用的上下文。
      • gcc 甚至可以预先计算很多模式。整数a = 0; for(int i=0;i
      • @Thomas:我必须同意 user3125280 - 应该比较这些语言在做某事 smart 方面的表现,而不是在做一些愚蠢的事情时未能击败真正的编程语言.智能算法通常不太关心微观效率,而是关心灵活性、连接(组合)和基础设施的能力。关键不在于获得 20 毫秒还是 50 毫秒,而是没有获得 8 秒或 8 分钟。
      【解决方案7】:

      我假设只有当涉及的数字有很多小因素时,因素的数量才会很大。所以我使用了 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,064 个三角形数172,032 个因子在 21 分 25 秒内(2.4GHz Core2 Duo),所以这个代码平均每个数字只需要 116 个处理器周期。最后一个三角数本身大于2^68,所以

      【讨论】:

        【解决方案8】:

        只是为了好玩。以下是更“原生”的 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;
        }
        

        在 main 中将 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加速。

        【讨论】:

        • 只是为了比较:原始c版本:9.1690,thaumkid的版本:0.1060 86x改进。
        • 哇。避免推断类型后,Haskell 表现出色
        • 其实不是推理造成的。这只会帮助您 A) 调试或避免类型问题和类型类实例选择问题 B) 使用一些现代语言扩展调试并避免一些无法确定的类型问题。它还可以帮助您使您的程序不可组合,这样您就永远无法扩大您的开发工作。
        • c 版本 0.11 s 上 Intel 骷髅峡谷
        【解决方案9】:

        我将“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$
        

        【讨论】:

          【解决方案10】:

          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;
          

          C 代码不这样做。通常,在相同代码的不同实现之间进行公平比较可能很棘手,特别是如果算法是数字的,因为您需要确保它们实际上在做同样的事情。由于某处的某种类型转换,一个实现中的轻微舍入错误可能会导致它比另一个实现更多的迭代,即使两者最终达到相同的结果。

          为了消除这个可能的错误源(并摆脱每次迭代中的额外测试),我重写了 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。即使在运行时实际上没有使用过 bignums,也必须执行这些检查。另一方面,这意味着你知道如果你突然给它比以前更大的输入,算法永远不会因为意外的整数环绕而失败。

          问题 4:我的功能实现是否允许 LCO,从而避免在调用堆栈中添加不必要的帧?

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

          【讨论】:

          • 我同意你的看法。由于多种原因,该基准并不精确,尤其是对于 Erlang
          【解决方案11】:

          使用 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
          

          如您所见,这个在同一台机器上运行了 38 毫秒,而您的机器运行了 16 秒 :)

          编译命令:

          ghc -O2 012.hs -o bin\012.exe
          ghc -O2 012_slow.hs -o bin\012_slow.exe
          

          【讨论】:

          • 上次我检查了 Haskell 的“素数”只是一个预先计算好的素数的巨大列表——没有计算,只是查找。所以是的,当然这会更快,但它没有告诉你在 Haskell 中导出素数的计算速度。
          • @zxq9 你能指出质数包源 (hackage.haskell.org/package/primes-0.2.1.0/docs/src/…) 中质数列表的位置吗?
          • 虽然来源显示素数不是预先计算的,但这种加速完全是疯狂的,比 C 版本快几英里,那么到底发生了什么?
          • 不,我的意思是map 迭代。 Haskell,即使没有循环,仍然允许迭代,但表示为递归调用。在这个解决方案中,我们调用了primeFactors,它可能在下面调用了primes。这个函数可能会返回可以记忆的素数列表,所以下一次调用将只计算缺失的素数顶部,而不是像 C 中的代码那样从下往上计算。
          • 是 1000 除数,而不是 500。
          【解决方案12】:

          在 x86_64 Core2 Duo (2.5GHz) 机器上使用 GHC 7.0.3gcc 4.4.6Linux 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。修复不会导致任何变化(编译器很聪明,你很幸运)。
          • 您使用了mod,其中rem 更快且足够。这会将时间更改为 8.5 秒
          • factorCount' 不断地应用两个永远不会改变的额外参数(numbersqrt)。工人/包装器转换为我们提供了:
           $ 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
          

          编辑:既然我们已经探索过了,让我们来解决问题

          问题一:erlang、python、haskell会因为使用而失去速度吗 任意长度的整数,或者只要值更小,它们就不会 比 MAXINT?

          在 Haskell 中,使用 IntegerInt 慢,但慢多少取决于执行的计算。幸运的是(对于 64 位机器)Int 就足够了。出于可移植性考虑,您可能应该重写我的代码以使用Int64Word64(C 不是唯一具有long 的语言)。

          问题2:为什么haskell这么慢?是否有编译器标志 关闭刹车还是我的实施? (后者相当 对我来说,haskell 可能是一本有七个印章的书。)

          问题 3:你能告诉我一些如何优化这些的提示吗? 实现而不改变我确定因素的方式? 以任何方式进行优化:更好、更快、更“原生”的语言。

          这就是我上面的回答。答案是

          • 0) 通过-O2 使用优化
          • 1) 尽可能使用快速(特别是:可拆箱)类型
          • 2) rem 不是 mod(一个经常被遗忘的优化)和
          • 3) worker/wrapper 转换(可能是最常见的优化)。

          问题 4:我的功能实现是否允许 LCO,因此 避免在调用堆栈中添加不必要的帧?

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

          【讨论】:

          • @Karl 因为rem 实际上是mod 操作的子组件(它们不一样)。如果您查看 GHC Base 库,您会看到 mod 测试了几个条件并相应地调整符号。 (见modInt#Base.lhs
          • 另一个数据点:我写了一个quick Haskell translation of the C program 没有看@Hyperboreus 的Haskell。所以它有点接近标准的惯用 Haskell,我故意添加的唯一优化是在阅读此答案后将 mod 替换为 rem(呵呵,哎呀)。请参阅链接了解我的时间安排,但简短版本“几乎与 C 相同”。
          • 即使 C 版本在我的机器上运行得更快,我现在对 Haskell 有了新的尊重。 +1
          • 这让我很惊讶,虽然我还没有尝试过。由于原始的factorCount' 是尾递归的,我原以为编译器可以发现额外的参数没有被更改,并仅针对更改的参数优化尾递归(毕竟,Haskell 是一种纯语言,这应该很容易)。有人认为编译器可以做到这一点,还是我应该回去阅读更多的理论论文?
          • @kizzx2:有一个GHC ticket 可以添加它。据我了解,这种转换可能会导致闭包对象的额外分配。这意味着在某些情况下性能更差,但正如 Johan Tibell suggests in his blog post 一样,如果可以内联生成的包装器,则可以避免这种情况。
          【解决方案13】:

          使用 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.49s) 比 @Thomas 回答中的版本 (~ 7.04s) 慢大约 6% 7.6.2,而来自@Raedwulf 的 C 版本运行 ~ 3.15s。 GHC 似乎在过去一年中有所改善。

          附言。我知道这是一个老问题,我从谷歌搜索中偶然发现了它(我忘记了我在搜索什么,现在......)。只是想评论一下关于 LCO 的问题,并表达我对 Haskell 的总体感受。我想评论最上面的答案,但是 cmets 不允许代码块。

          【讨论】:

            【解决方案14】:

            问题 3:您能否给我一些提示如何优化这些实现 不改变我确定因素的方式?任何优化 方式:更好、更快、更“原生”的语言。

            C 实现并不理想(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 通常会生成更快的代码。

            【讨论】:

            • 非常好!作为比较,在我的机器上,您的 C 解决方案在 2.5 seconds 中运行,而对 Haskell 代码的类似修改(移动到 Word32,添加 INLINE pragma)导致运行时间为 4.8 seconds。或许可以做点什么(似乎不是微不足道的)——gcc 结果确实令人印象深刻。
            • 谢谢!也许问题应该是各种编译器编译输出的速度,而不是实际语言本身。话又说回来,拿出英特尔手册并手动优化仍然会完全获胜(只要您有知识和时间(很多))。
            【解决方案15】:

            查看您的 Erlang 实现。时间包括整个虚拟机的启动、运行程序和停止虚拟机。我很确定设置和停止 erlang 虚拟机需要一些时间。

            如果计时是在 erlang 虚拟机本身内完成的,结果会有所不同,因为在这种情况下,我们将只有相关程序的实际时间。否则,我相信启动和加载 Erlang Vm 的过程所花费的总时间加上停止它所花费的总时间(正如你把它放在你的程序中一样)都包括在你用来计时的方法的总时间中程序正在输出。考虑使用 erlang 计时本身,当我们想要在虚拟机本身内对程序计时时使用它 timer:tc/1 or timer:tc/2 or timer:tc/3。这样,erlang 的结果将排除启动和停止/杀死/停止虚拟机所花费的时间。这就是我的推理,考虑一下,然后再次尝试你的基准。

            我实际上建议我们尝试在这些语言的运行时内对程序(对于具有运行时的语言)进行计时,以获得精确的值。例如,C 没有像 Erlang、Python 和 Haskell 那样启动和关闭运行时系统的开销(98% 的肯定——我支持修正)。所以(基于这个推理)我总结说这个基准对于运行在运行时系统之上的语言来说不够精确/公平。让我们通过这些更改再做一次。

            编辑:即使所有语言都有运行时系统,启动和停止它的开销也会有所不同。所以我建议我们从运行时系统中计时(对于这适用的语言)。众所周知,Erlang VM 在启动时有相当大的开销!

            【讨论】:

            • 我忘了在我的帖子中提到它,但我确实测量了启动系统所需的时间 (erl -noshell -s erlang halt) - 在我的机器上大约 0.1 秒。与程序的运行时间(大约 10 秒)相比,这已经足够小了,不值得争论。
            • 在你的机器上!我们不知道您是否在使用 sun fire 服务器!由于时间是与机器规格成正比的变量,因此应将其考虑在内.... 狡辩?
            • @RichardC 没有人提到 Erlang 更快 :) 它有不同的目标,而不是速度!
            【解决方案16】:
            问题 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 谢谢!另外,我真的很好奇你的下一个问题。但是,我警告你,我的知识有限,所以我无法回答你的每一个问题。为了弥补它,我创建了我的答案社区 wiki,以便人们可以更轻松地对其进行补充。
            • 关于使用范围。当我用带增量的 while 循环替换范围时(模仿 C 的 for 循环),执行时间实际上加倍。我猜生成器已经相当优化了。
            【解决方案17】:

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

            说到 Python 速度,您使用的是错误的实现!试试PyPy,对于这样的事情,你会发现它要快得多。

            【讨论】:

            • 博客链接已失效。
            猜你喜欢
            • 2020-08-06
            • 2019-03-07
            • 1970-01-01
            • 2020-05-10
            • 1970-01-01
            • 2017-11-15
            • 1970-01-01
            • 2011-02-19
            • 2016-06-25
            相关资源
            最近更新 更多