Project Eulerとの速度比較:C vs Python vs Erlang vs Haskell


670

私はProject Eulerからの問題#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

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%(PyPyでは118%)
  • Erlang:436%(RichardCのおかげで135%)
  • Haskell:1421%

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

質問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

55
@Jochen(およびSeth)Cが高速または素晴らしいというわけではありませんが、パフォーマンスの高いコードを書くのは簡単であると認識されています(これは当てはまらないかもしれませんが、ほとんどのプログラムは可能であるように思われるため、十分に当てはまります)。私の回答を調べてみると、時間とともに真実であることがわかったので、選択した言語の一般的な最適化に関するプログラマのスキルと知識は非常に重要です(特にHaskellの場合はそうです)。
Thomas M. DuBuisson、2011

52
ただで確認Mathematicaに - それは0.25secかかる(Cでそれがここ6secかかります)、およびコードはちょうどです:Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]。ばんざーい!
tsvikas

35
Cとアセンブリの間のこれらの戦争を覚えている人は他にいますか?「確かに、Cでコードを10倍速く書くことができますが、Cコードはこれほど速く実行できますか?...」マシンコードとアセンブリの間で同じ戦いが繰り広げられたと私は確信しています。
JS。

39
@JS:おそらくそうではありません。アセンブリは、生のバイナリマシンコードの代わりに、単に入力するニーモニックのセットなので、通常は1対1で対応しています。
Callum Rogers

9
結論、Haskellの場合:-O2は約3倍の高速化を実現し、Integerの代わりにIntを使用すると、約4倍から6倍になり、合計速度は12倍から14倍以上になります。
Will Ness

回答:


794

使用してGHC 7.0.3gcc 4.4.6Linux 2.6.29、x86_64のコア2デュオ(2.5GHz帯)マシンで使用してコンパイルghc -O2 -fllvm -fforce-recompのHaskellおよびgcc -O3 -lmCについて

  • Cルーチンは8.4秒で実行されます(おそらくのため、実行よりも高速です-O3
  • Haskellソリューションは36秒で実行されます(-O2フラグにより)
  • あなたのfactorCount'コードは明示的にタイプされておらず、デフォルトに設定されていませんInteger(ここで私の誤診を修正してくれたDanielに感謝します!)。使用して明示的な型シグネチャ(とにかく標準的な方法)を指定する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で十分です。移植性のために、おそらく使用するように私のコードを書き直す必要があります(Int64またはを使用するWord64言語はCだけではありませんlong)。

質問2:haskellが非常に遅いのはなぜですか?ブレーキをオフにするコンパイラフラグはありますか、それとも私の実装ですか?(ハスケルは私に7つのシールを付けた本なので、後者はかなりありそうです。)

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

それが私が上で答えたものです。答えは

  • 0)経由で最適化を使用する -O2
  • 1)可能な場合は高速(特に:ボックス化できない)タイプを使用する
  • 2)しremないmod(頻繁に忘れられる最適化)および
  • 3)ワーカー/ラッパー変換(おそらく最も一般的な最適化)。

質問4:機能の実装ではLCOが許可されているため、不要なフレームをコールスタックに追加することはできませんか?

はい、それは問題ではありませんでした。良い仕事で、これを検討してくれてうれしいです。


25
@Karl remは実際にはmod操作のサブコンポーネントです(これらは同じではありません)。GHCベースライブラリを見るとmod、いくつかの条件のテストが表示され、それに応じて符号が調整されます。(参照modInt#してBase.lhs
トーマス・M. DuBuisson

20
別のデータポイント:私は、@ HyperboreusのHaskellを見ずに、Cプログラムの簡単なHaskell変換を書きました。つまり、標準的な慣用的なHaskellに少し近づいており、私が故意に追加した唯一の最適化は、この回答を読んmodrem後で置き換えることです(へえ、おっと)。私のタイミングについてはリンクを参照してください。ただし、ショートバージョンは「Cとほとんど同じ」です。
CAマッキャン2011

106
私のマシンではCバージョンの方が高速であると思っていましたが、今はHaskellに新しい敬意を持っています。+1
セスカーネギー

11
私はまだ試していませんが、これは私にはかなり驚くべきことです。オリジナルfactorCount'はテール再帰的だったので、コンパイラーが変更されていない余分なパラメーターを見つけて、変更中のパラメーターに対してのみテール再帰を最適化できると思ったでしょう(結局、Haskellは純粋な言語なので、これは簡単なはずです)。コンパイラがそれを行うことができると誰もが思うでしょう、または私は理論論文をもっと読むべきですか?
kizzx2

22
@ kizzx2:追加するGHCチケットがあります。私が理解したところによると、この変換はクロージャーオブジェクトの追加の割り当てをもたらす可能性があります。これは場合によってはパフォーマンスが低下することを意味しますが、Johan Tibell が彼のブログ投稿で示唆しているように、結果のラッパーをインライン化できる場合はこれを回避できます。
ハマー2011

224

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コードはこれを行いません。一般に、同じコードの異なる実装間で公平な比較を行うのは難しい場合があります。特にアルゴリズムが数値の場合は、実際に同じことを実行していることを確認する必要があるためです。いずれかの型キャストに起因する1つの実装でのわずかな丸めエラーにより、どちらも最終的に同じ結果に到達したとしても、他よりも多くの反復が行われる可能性があります。

この考えられるエラーの原因を排除する(そして各反復での余分なテストを取り除く)ために、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ビット演算を使用する」と言う方法はないので、コンパイラが整数の境界を証明できない場合(通常はできない)、すべての計算をチェックして確認する必要がありますタグ付けされた単一の単語に収まる場合、またはヒープに割り当てられたビッグナムに変換する必要がある場合。実行時に実際にbignumが使用されていない場合でも、これらのチェックを実行する必要があります。一方、その手段は、あなたが知っている、あなたが突然、以前よりもそれを大きな入力を与えた場合、アルゴリズムは、予期しない整数のラップアラウンドで失敗することはありませんことを。

質問4:機能の実装ではLCOが許可されているため、不要なフレームを呼び出しスタックに追加することはできませんか?

はい、あなたのErlangコードはラストコール最適化に関して正しいです。


2
仰るとおりです。このベンチマークは、特にいくつかの理由でErlangにとって正確ではありませんでした
Muzaaya Joshua

156

Pythonの最適化に関しては、PyPyを使用することに加えて(コードをまったく変更せずに大幅に高速化するため)、PyPyの翻訳ツールチェーンを使用してRPython準拠のバージョンをコンパイルするか、Cythonを使用して拡張モジュールを構築できます。私のテストではCバージョンよりも高速で、Cythonモジュールはほぼ2倍高速です。参考までに、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 euler12-rpython.pyCに翻訳してコンパイルするtranslate.pyを使用して翻訳できます。

# 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を集中的に使用するコードを記述することは、プログラムを最適化するための非常に簡単な方法のようです。


6
私は好奇心が強いのですが、Cバージョンは少なくともCPythonと同じくらい高速になるように最適化できますか?
表示名

4
それはあなたが確認C.のうち、そのパフォーマンスを得ることができることを意味し、高度に最適化されたCソースにダウンコンパイルするので、Cythonのバージョンは、非常に高速であることを@SargeBorsch
イーライKorvigo

72

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

Cの実装は最適ではなく(Thomas M. DuBuissonが示唆)、バージョンは64ビット整数(つまりlongデータ型)を使用します。後でアセンブリリストを調査しますが、経験に基づいた推測では、コンパイルされたコードでいくつかのメモリアクセスが行われているため、64ビット整数の使用が大幅に遅くなっています。それまたは生成されたコードです(SSEレジスターに64ビットのintをより少なく収めたり、doubleを64ビットの整数に丸めることができるという事実は遅いです)。

これが変更されたコードです(単にlongintに置き換え、明示的に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 secondsていますが、Haskellコードへの同様の変更(Word32への移動、INLINEプラグマの追加)の結果、ランタイムになり4.8 secondsます。おそらく何かを行うことができます(些細なことではないようです)-gccの結果は確かに印象的です。
Thomas M. DuBuisson、2011

1
ありがとう!おそらく問題は、実際の言語自体ではなく、さまざまなコンパイラによってコンパイルされた出力の速度であるべきです。繰り返しになりますが、Intelのマニュアルを引き出して手動で最適化しても、完全に成功します(知識と時間(多くの)がある場合)。
Raedwulf、2011

56

見てみましょうこのブログを。過去1年ほどの間、彼はHaskellとPythonでProject Eulerの問題のいくつかを実行してきましたが、一般的にHaskellの方がはるかに高速であることがわかっています。それらの言語の間では、あなたの流暢さとコーディングスタイルにもっと関係があると思います。

Pythonの速度に関しては、間違った実装を使用しています!PyPyを試してみてください。このようなことをすると、はるかに速くなることがわかります。


32

Haskellパッケージの一部の関数を使用すると、Haskellの実装を大幅に高速化できます。この場合、 '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

5
最後に、Haskellの「素数」が事前計算された素数の膨大なリストにすぎないことを確認しました。計算はせず、ルックアップだけです。ですから、もちろん、これはより高速になりますが、Haskellで素数を導出する計算速度については何もわかりません。
zxq9 2015

21
@ zxq9は、素数パッケージのソース(hackage.haskell.org/package/primes-0.2.1.0/docs/src/…)のどこに素数のリストがあるのか教えていただけませんか?
Fraser

4
ソースは素数が事前計算されていないことを示していますが、この高速化はまったく正気でなく、Cバージョンよりもはるかに速いため、一体何が起こっているのでしょうか。
セミコロン

1
@セミコロン暗記。この場合、Haskellは実行時にすべての素数を記憶したので、反復ごとに素数を再計算する必要はないと思います。
Hauleth 2016年

5
それは1000の除数ではなく、500です
キャスパーFærgemand

29

楽しみのためだけに。以下は、より「ネイティブな」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 gcc -O3 -lmで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高速化を興味のある読者に任せています。


3
比較のためだけに:オリジナルのcバージョン:9.1690、thaumkidのバージョン:0.1060 86倍の改善。
1

ワオ。Haskellは、推論された型を回避すると優れたパフォーマンスを発揮します
Piyush Katariya 2018

実際、それを行ったのは推論ではありません。これは、A)型の問題と型クラスインスタンスの選択の問題をデバッグまたは回避することだけを支援します。また、プログラムを合成不可能にして、開発作業を拡大することもできなくなります。
codeshot、2018

cバージョン0.11 s、Intel skullキャニオン
コードショット2018

13
質問1:任意の長さの整数を使用しているため、erlang、python、およびhaskellの速度は緩んでいますか、または値がMAXINT未満である限り、そうではありませんか?

これはありそうもない。ErlangとHaskellについてはあまり説明できませんが(以下、Haskellについては少しかもしれません)、Pythonの他の多くのボトルネックを指摘できます。プログラムがPythonのいくつかの値を使用して操作を実行しようとするたびに、値が適切なタイプのものかどうかを確認する必要があり、少し時間がかかります。あなたのfactorCount関数はただでリストを割り当てrange (1, isquare + 1)、様々な時間、および実行時、mallocあなたは特にC.で行うよう-styledメモリ割り当ては、カウンターで道に遅い範囲で反復するよりも、factorCount()複数回呼び出さので、リストの多くを割り当てています。また、Pythonが解釈され、CPythonインタープリターが最適化に重点を置いていないことも忘れないでください。

編集:ああ、まあ、私はあなたがPython 3を使用しているのでrange()、リストではなくジェネレータを返すことに注意してください。この場合、リストの割り当てに関する私の指摘は半分間違っています:関数rangeはオブジェクトを割り当てるだけですが、それでも非効率的ですが、多くのアイテムを含むリストを割り当てるほど非効率的ではありません。

質問2:haskellが非常に遅いのはなぜですか?ブレーキをオフにするコンパイラフラグはありますか、それとも私の実装ですか?(ハスケルは私に7つのシールを付けた本なので、後者はかなりありそうです。)

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を作成しました。
brandizzi

使用範囲について。範囲をインクリメントのあるwhileループ(Cのforループを模倣)に置き換えると、実行時間が実際に2倍になります。ジェネレーターはかなり最適化されていると思います。
Hyperboreus、2011

12

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の回答の明示的な再帰を一般的なリスト操作に置き換えました。コードは、末尾再帰を心配することなく、まったく同じことを行います。これは、(〜走る7.49s程度)6%遅く@Thomas'と答えた(〜でのバージョンよりも7.04s @RaedwulfからCのバージョンは〜実行中に、GHC 7.6.2と私のマシン上で)3.15sを。GHCは1年で改善したようです。

PS。私はそれが古い質問であることを知っています、そして私はグーグル検索からそれを偶然見つけました(私は今私が探していたものを忘れていました...)。LCOについての質問にコメントし、Haskell全般に対する私の感情を表現したかっただけです。上位の回答についてコメントしたかったのですが、コメントではコードブロックが許可されません。


9

Cバージョンのいくつかの数字と説明。どうやら誰もそれらの年の間にそれをしなかった。誰もが見て、学ぶことができるように、この回答に賛成することを忘れないでください。

ステップ1:著者のプログラムのベンチマーク

ラップトップの仕様:

  • CPU i3 M380(931 MHz-最大バッテリー節約モード)
  • 4GBメモリ
  • Win7 64ビット
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwinと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は今のところ元のプログラムと同じです(詳細は後で説明します)。
  • アーキテクチャはコンパイラ設定に応じてx86またはx64です
  • コンパイラはgccまたはvs2012です

ステップ2:再度調査、改善、ベンチマーク

VSはgccより250%高速です。2つのコンパイラは同様の速度を提供します。明らかに、コードまたはコンパイラオプションに問題があります。調べよう!

最初の興味深い点は整数型です。変換にはコストがかかる可能性があり、コードの生成と最適化を改善するには整合性が重要です。すべての整数は同じ型でなければなりません。

これは、混合の混乱だ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

整数型でありint long int32_t uint32_t int64_tかつuint64_tから#include <stdint.h>

Cにはたくさんの整数型があり、さらにいくつかの符号付き/符号なしで操作でき、さらにx86またはx64としてコンパイルする選択肢があります(実際の整数サイズと混同しないでください)。それはコンパイルして実行するための多くのバージョンです^^

ステップ3:数字を理解する

決定的な結論:

  • 32ビット整数は64ビット整数よりも200%高速
  • 符号なし64ビット整数が25%高速化よりも符号付き64ビット(残念ながら、私はそのために何の説明がありません)

トリックの質問:「Cのintとlongのサイズは?」
正解は次とおりです。Cのintとlongのサイズは明確に定義されていません。

C仕様から:

intは少なくとも32ビット
長で、少なくともint

gccのmanページから(-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 ++でも十分に定義されておらず、コンパイラやアーキテクチャによって異なります。一貫性と予測可能性が必要な場合は、のuint32_t整数ファミリーを使用してください#include <stdint.h>

  • 速度の問題が解決されました。他のすべての言語は何百パーセントも遅れており、C&C ++が再び勝利しました!彼らはいつもそうします。次の改善は、OpenMP:Dを使用したマルチスレッド化です。


好奇心から、インテルのコンパイラーはどのように機能しますか?彼らは通常、数値コードの最適化に非常に優れています。
kirbyfan64sos

C仕様で「intが少なくとも32ビットである」ことが保証されているというリファレンスはどこにありますか?私が知っている唯一の保証は、INT_MINand の最小値INT_MAX(-32767と32767であり、実際intには少なくとも16ビットである必要があります)です。longは少なくともと同じ大きさであるint必要があり、範囲要件の平均longは少なくとも32ビットです。
ShadowRanger 2017年


8

Erlangの実装を確認します。タイミングには、仮想マシン全体の起動、プログラムの実行、仮想マシンの停止が含まれます。erlang vmのセットアップと停止には時間がかかると確信しています。

タイミングがerlang仮想マシン自体の内部で行われた場合、結果は異なります。その場合、問題のプログラムのみの実際の時間があります。そうでなければ、Erlang Vmの起動とロードのプロセスにかかる合計時間とそれを停止する(プログラムに入れた)プロセスの合計時間はすべて、あなたが時間を計るために使用しているメソッドの合計時間に含まれていると思いますプログラムが出力しています。仮想マシン自体の中でプログラムの時間を計測したいときに使用するアーランタイミング自体の使用を検討してください timer:tc/1 or timer:tc/2 or timer:tc/3。このようにして、erlangからの結果には、仮想マシンの起動と停止/終了/停止にかかる時間が含まれていません。それが私の推論です。考えてから、もう一度ベンチマークを試してください。

実際に、正確な値を取得するために、プログラムの時間を(ランタイムのある言語の場合)、それらの言語のランタイム内で調整することをお勧めします。たとえばCは、Erlang、Python、Haskellのようにランタイムシステムを起動およびシャットダウンするオーバーヘッドがありません(これは98%確実です-私は修正を意味します)。(この推論に基づいて)結論として、このベンチマークは、ランタイムシステム上で実行される言語に対して/ fairが正確ではなかったと結論付けています。これらの変更を使ってもう一度やってみましょう。

編集:すべての言語にランタイムシステムがあったとしても、それぞれを開始して停止するオーバーヘッドは異なります。したがって、ランタイムシステム内から時間をとることをお勧めします(これが適用される言語用)。Erlang VMは起動時にかなりのオーバーヘッドがあることが知られています!


私は投稿でそれを言及するのを忘れていましたが、システムを起動するだけにかかる時間を測定しました(erl -noshell -s erlang halt)-私のマシンで約0.1秒。これは、プログラムの実行時間(約10秒)と比較して十分に小さいので、おかしくはありません。
RichardC、2011

あなたのマシンで!Sun Fireサーバーで作業しているかどうかはわかりません!。時間はマシンの仕様に比例する変数であるため、考慮に入れる必要があります。
ムザヤジョシュア

2
@RichardC Erlangの方が速いとはどこにも言われていません:)速度ではなく、目標が異なります!
2013

7

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

質問1は、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で完了
Mark Washeim

あなたの結果:76576500他のすべての人:842161320そこはあなたの結果に問題があります
davidDavidson 2014年

他のオイラー問題をドンチェックしていたので、結果を確認したところです。projecteuler.net/problem=12への答えは76576500です。奇妙に思えますが、チェックしたところです。
Mark Washeim

比較のために、元のcバージョンで9.03を取得し、Erlang 19をMarkのコードで使用すると、5.406、167.0366%速くなります。
1

5

C ++ 11、20ミリ秒未満 - ここで実行

言語固有の知識を向上させるためのヒントが欲しいとのことですが、ここでは十分にカバーされているので、あなたの質問に対するmathematicaのコメントを見た人のためにいくつかのコンテキストを追加したいと思い、なぜこれが疑問に思いましたかコードはとても遅くなりました。

この答えは主に、質問/その他の答えのコードをより簡単に評価できるようにコンテキストを提供するためのものです。

このコードは、以下に基づいて、使用されている言語に関係のない、いくつかの(醜い)最適化のみを使用します。

  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ミリ秒かかります。これは、ここで見た他のほとんどのコードとはかけ離れています。そして、間違いなく、多くの最適化がまだ利用可能です。


7
これは、「4つの言語で可能な限り同じアルゴリズムを実際に実装しようとしたのです」という質問者の要求とは明らかに異なります。あなたに似た多くの削除された回答の1つについてコメントを引用するには、「言語に関係なく、より優れたアルゴリズムでより高速に処理できることは明らかです」。
Thomas M. DuBuisson 2014

2
@ ThomasM.DuBuisson。それは私が得ているものです。質問と回答は、アルゴリズムの高速化が重要であることを強く示唆しています(もちろん、OPはそれらを要求していません)。しかし、明示的な例はありません。この答え-厳密に最適化されたコードではない-は、OPのコードがどれほど遅い/速いか疑問に思っていた私のような誰にとっても、少し便利なコンテキストを提供します。
user3125280 2014

gccは、多くのパターンを事前に計算することもできます。int a = 0; for(int i = 0; i <10000000; ++ i){a + = i;}はコンパイル時に計算されるため、実行時に1ミリ秒未満かかります。それも数える
アーサー

@Thomas:私はuser3125280に同意する必要があります- 何かばかげたことで実際のプログラミング言語を打ち負かすのに失敗するのではなく、言語を比較して、賢いことをする必要があります。通常、スマートアルゴリズムは、柔軟性、物事を結び付ける機能(それらを組み合わせる)、インフラストラクチャーよりも、微視的な効率を重視しません。20ミリ秒でも50ミリ秒でも、8秒や8分ではありません
DarthGizka 2016年

5

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%
go: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%
2番目のバージョン: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)

1

変化する: case (divisor(T,round(math:sqrt(T))) > 500) of

に: case (divisor(T,round(math:sqrt(T))) > 1000) of

これにより、Erlangマルチプロセスの例に対する正しい答えが生成されます。


2
これはこの回答へのコメントを意図したものでしたか?それは明確ではないので、これはそれ自体で答えではありません。
ShadowRanger 2017年

1

関係する数に多くの小さな因子がある場合にのみ、因子の数が多いと仮定しました。したがって、私はthaumkidの優れたアルゴリズムを使用しましたが、最初に、係数が小さすぎることのない係数の概算を使用しました。それは非常に簡単です:29までの素因数をチェックしてから、残りの数をチェックして、要素の数の上限を計算します。これを使用して因子の数の上限を計算し、その数が十分に高い場合は、因子の正確な数を計算します。

以下のコードは、正確性のためにこの仮定を必要としませんが、高速です。うまくいくようです。100,000の数のうち約1つだけが、完全なチェックを必要とするのに十分な高さの見積もりを与えます。

コードは次のとおりです。

// 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番目の三角形、および係数が172,032の26,467,792,064番目の三角形が見つかります。 21分25秒(2.4GHz Core2 Duo)なので、このコードは、平均で数あたり116プロセッササイクルしかかかりません。最後の三角形の数自体は2 ^ 68より大きいので、


0

「Jannich Brendle」バージョンを500ではなく1000に変更しました。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$

0
#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.79秒ユーザー0.00秒システム99%cpu 2.794合計

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.