Project Euler による速度比較: C vs Python vs Erlang vs Haskell 質問する

Project Euler による速度比較: C vs Python vs Erlang vs Haskell 質問する

取りました問題 12からプロジェクト オイラープログラミングの練習として、また C、Python、Erlang、Haskell での (もちろん最適ではない) 実装を比較するために。実行時間を短縮するために、元の問題で述べられている 500 ではなく、1000 を超える約数を持つ最初の三角数を検索します。

結果は以下のようになります。

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

パイソン:

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

アーラン:

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

まとめ:

  • 100% ...
  • Python: 692% (PyPy 使用時は 118%)
  • Erlang: 436% (135% は RichardC のおかげです)
  • ハスケル: 1421%

C は、他の 3 つのように任意の長さの整数ではなく、計算に long を使用するため、大きな利点があると思います。また、最初にランタイムをロードする必要もありません (他の言語ではどうでしょうか?)。

質問 1: Erlang、Python、Haskell は、任意の長さの整数を使用することで速度が低下しますか、それとも値が 未満である限り速度は低下しませんかMAXINT?

質問 2: Haskell はなぜこんなに遅いのでしょうか? ブレーキをオフにするコンパイラ フラグがあるのでしょうか、それとも私の実装の問題なのでしょうか? (Haskell は私にとって 7 つの封印がされた本なので、後者である可能性が非常に高いです。)

質問 3:要素を決定する方法を変えずに、これらの実装を最適化する方法について、ヒントを教えていただけますか? あらゆる方法での最適化: より良く、より速く、より言語に「ネイティブ」な最適化。

編集:

質問 4:機能実装では LCO (最終呼び出し最適化、別名末尾再帰除去) が許可され、呼び出しスタックに不要なフレームが追加されないようにしていますか?

私の Haskell と Erlang の知識は非常に限られていることを認めざるを得ませんが、4 つの言語で可能な限り同じアルゴリズムを実装しようと本当に努力しました。


使用されたソースコード:

#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

ベストアンサー1

x86_64 Core2 Duo (2.5GHz) マシンで、、GHC 7.0.3を使用してgcc 4.4.6、Haskell の場合は、およびC の場合は、を使用してコンパイルします。Linux 2.6.29ghc -O2 -fllvm -fforce-recompgcc -O3 -lm

  • C ルーチンは 8.4 秒で実行されます (おそらく のせいで、実行時間よりも速いです-O3)
  • Haskellソリューションは36秒で実行されます(-O2フラグのため)
  • あなたのfactorCount'コードは明示的に型指定されておらず、デフォルトで になっていますInteger(ここでの私の誤診を訂正してくれたダニエルに感謝します!)。明示的な型シグネチャ(とにかく標準的な方法)を使用するとInt、時間が11.1秒に変わります。
  • では、factorCount'不必要に を呼び出していますfromIntegral。ただし、修正しても何も変わりません (コンパイラは賢いので、幸運です)。
  • modより高速かつ十分な場所を使用しましたrem。これにより、時間が8.5 秒に変更されます。
  • factorCount'常に変化しない 2 つの追加引数 ( numbersqrt) を適用します。ワーカー/ラッパー変換により次のようになります。
 $ time ./so
 842161320  

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

そうです、7.95 秒です。Cソリューションよりも一貫して 0.5 秒高速です-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を使用するように書き直す必要があります( を持つ言語は C だけではありません)。Int64Word64long

質問 2: Haskell はなぜこんなに遅いのでしょうか? ブレーキをオフにするコンパイラ フラグがあるのでしょうか、それとも私の実装の問題なのでしょうか? (Haskell は私にとって 7 つの封印がされた本なので、後者である可能性が非常に高いです。)

質問 3: 要素を決定する方法を変えずに、これらの実装を最適化する方法について、ヒントを教えていただけますか? あらゆる方法での最適化: より良く、より速く、より言語に「ネイティブ」な最適化。

それが私が上で答えたことです。答えは

  • 0) 最適化を使用する-O2
  • 1) 可能な場合は高速な(特にunbox可能な)型を使用する
  • 2) remmod忘れられがちな最適化)
  • 3) ワーカー/ラッパー変換 (おそらく最も一般的な最適化)。

質問 4: 機能実装では LCO が許可され、呼び出しスタックに不要なフレームが追加されないようにしていますか?

はい、それは問題ではありませんでした。素晴らしい仕事でしたし、この点を考慮していただいて嬉しいです。

おすすめ記事