Project Euler와의 속도 비교 : C vs Python vs Erlang vs Haskell


672

Project Euler의 문제 # 12 를 프로그래밍 연습으로 취하고 C, Python, Erlang 및 Haskell의 ( 실제로는 최적이 아닌) 구현을 비교했습니다. 더 높은 실행 시간을 얻기 위해 원래 문제에서 설명한대로 500 대신 1000이 아닌 제수가있는 첫 번째 삼각형 수를 검색합니다.

결과는 다음과 같습니다.

씨:

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

요약:

  • C : 100 %
  • Python : 692 % (PyPy 사용시 118 %)
  • 얼랭 : 436 % (RichardC 덕분에 135 %)
  • 하스켈 : 1421 %

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의 경우).
토마스 M. DuBuisson

52
그냥 확인할 티카 - 그것은 0.25sec 소요 (C와 여기에 6 초 소요), 그리고 코드는 다음과 같습니다 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를 사용하면 총 속도가 12x-14x 이상인 경우 약 4x-6x가됩니다.
윌 네스

답변:


794

사용하여 GHC 7.0.3, gcc 4.4.6, Linux 2.6.29, x86_64의 코어 2 듀오 (2.5GHz의) 시스템에서 사용 컴파일 ghc -O2 -fllvm -fforce-recomp하스켈과 gcc -O3 -lmC.에 대한

  • C 루틴은 8.4 초 만에 실행됩니다 (으로 인해 달리는 것보다 빠름 -O3)
  • Haskell 솔루션은 -O2플래그 로 인해 36 초 안에 실행됩니다.
  • 귀하의 factorCount'코드는 명시 적으로 입력되지 않았으며 기본값으로 설정되지 않았습니다 Integer(여기서 오진 진단을 해준 Daniel에게 감사드립니다!). 사용하여 명시 적 유형 서명 (어쨌든 표준 연습)을 제공 Int하고 시간이 11.1 초로 변경됩니다.
  • factorCount'당신 불필요하게 불렀다 fromIntegral. 그래도 수정 사항은 변경되지 않습니다 (컴파일러는 똑똑하고 운이 좋습니다).
  • 더 빠르고 충분한 mod곳을 사용했습니다 rem. 시간이 8.5 초로 변경됩니다 .
  • factorCount'절대 변하지 않는 두 개의 추가 인수를 지속적으로 적용하고 있습니다 ( number, sqrt). 작업자 / 래퍼 변환은 다음을 제공합니다.
 $ time ./so
 842161320  

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

맞습니다, 7.95 초 . C 솔루션보다 0.5 초 빠르게 일관 됩니다. -fllvm플래그가 없으면 여전히 8.182 seconds이므로 NCG 백엔드 도이 경우에도 잘 수행됩니다.

결론 : 하스켈은 대단합니다.

결과 코드

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이 속도를 잃습니까?

Haskell에서는 사용 Integer속도가 느리지 Int만 수행 속도는 계산 속도에 따라 다릅니다. 운 좋게도 (64 비트 시스템의 경우) Int충분합니다. 이식성을 위해서 당신은 아마 사용하려면 코드를 다시 작성해야 Int64또는 Word64(C는 유일한 언어가 아닙니다 long).

질문 2 : 왜 하스켈이 그렇게 느립니까? 브레이크를 끄는 컴파일러 플래그가 있습니까, 아니면 내 구현입니까? (하스켈은 나에게 7 개의 인장이있는 책이기 때문에 후자는 상당히 가능성이 높다.)

질문 3 : 요소를 결정하는 방식을 변경하지 않고 이러한 구현을 최적화하는 방법에 대한 힌트를 제공 할 수 있습니까? 어떤 방식 으로든 최적화 : 언어에 대해 더 좋고 빠르며 "네이티브"합니다.

그것이 내가 위에서 대답 한 것입니다. 대답은

  • 0) 통해 최적화 사용 -O2
  • 1) 가능하면 빠른 (특히 : unboxable) 유형을 사용하십시오.
  • 2) rem하지 않음 mod(자주 잊어 버린 최적화)
  • 3) 작업자 / 래퍼 변환 (아마도 가장 일반적인 최적화).

질문 4 : 기능 구현으로 LCO가 허용되므로 호출 스택에 불필요한 프레임을 추가하지 않습니까?

예, 그게 문제가 아니 었습니다. 잘하고 당신이 이것을 고려 기쁘게 생각합니다.


25
@Karl 왜냐하면 rem실제로 작업의 하위 구성 요소 이기 때문에 mod동일하지 않습니다. GHC Base 라이브러리를 보면 mod몇 가지 조건에 대한 테스트가 표시되고 그에 따라 부호가 조정됩니다. (참조 modInt#에서 Base.lhs)
토마스 M. DuBuisson에게

20
또 다른 데이터 포인트 : @Hyperboreus의 Haskell을 보지 않고 C 프로그램의 빠른 Haskell 번역을 작성했습니다 . 따라서 표준 관용적 Haskell에 조금 더 가깝고, 내가 추가 한 유일한 최적화는 이 답변을 읽은 후에 대체 mod하는 rem것입니다 (허, 죄송합니다). 내 타이밍에 대한 링크를 참조하십시오. 그러나 짧은 버전은 "거의 C와 거의 같습니다".
CA McCann

106
C 버전이 내 컴퓨터에서 더 빨리 실행된다고 생각했지만 Haskell에 대해 새로운 존경을 받고 있습니다. +1
세스 카네기

11
아직 시도하지는 않았지만 이것은 놀랍습니다. 원래 factorCount'는 꼬리 재귀 이기 때문에 컴파일러가 변경되지 않는 추가 매개 변수를 발견하고 변경 매개 변수에 대해서만 꼬리 재귀를 최적화 할 수 있다고 생각했을 것입니다 (결국 순수한 언어 인 Haskell은 쉬워야합니다). 누구든지 컴파일러가 그렇게 할 수 있다고 생각합니까? 아니면 더 많은 이론 논문을 읽으 러 가야합니까?
kizzx2

22
@ kizzx2 : 추가 할 GHC 티켓 이 있습니다. 내가 이해 한 바에 따르면,이 변환은 클로저 객체를 추가로 할당 할 수 있습니다. 이는 경우에 따라 성능이 저하되는 것을 의미하지만 Johan Tibell 이 블로그 게시물에서 제안한 것처럼 결과 래퍼를 인라인 할 수 있으면 피할 수 있습니다.
hammar

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 코드는 이것을하지 않습니다. 일반적으로 동일한 코드의 다른 구현간에, 특히 알고리즘이 수치 인 경우, 실제로 동일한 작업을 수행하고 있는지 확인해야하기 때문에 공정하게 비교하는 것은 까다로울 수 있습니다. 어딘가에 어떤 타입 캐스트로 인해 한 구현에서 약간의 반올림 오류가 발생하면 둘 다 동일한 결과에 도달하더라도 다른 것보다 더 많은 반복을 수행 할 수 있습니다.

이 가능한 오류 소스를 제거하고 각 반복에서 추가 테스트를 제거하기 위해 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.

이 재 작성 no 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에는 "wrap-around와 함께 32/64 비트 산술 사용"이라고 말하는 방법이 없으므로, 컴파일러가 정수에 대한 일부 한계를 증명할 수 없으면 (그리고 일반적으로 할 수없는 경우) 모든 계산을 확인해야합니다. 단일 태그 단어에 들어갈 수 있거나 힙 할당 큰 숫자로 바꾸어야하는 경우. 실제로 런타임에 큰 숫자를 사용하지 않더라도 이러한 검사를 수행해야합니다. 반면에, 그 의미는 당신이 알고 갑자기 이전보다 그에게 더 큰 입력을 주면 알고리즘 때문에 예기치 않은 정수 랩 어라운드 실패하지 않을 것입니다.

질문 4 : 기능 구현으로 LCO가 허용되므로 호출 스택에 불필요한 프레임을 추가하지 않습니까?

예, Erlang 코드는 마지막 통화 최적화와 관련하여 정확합니다.


2
동의합니다. 이 벤치 마크는 여러 가지 이유로 특히 Erlang에 대해 정확하지 않았습니다.
Muzaaya Joshua

156

Python 최적화와 관련하여 PyPy를 사용하는 것 외에도 (코드를 전혀 변경하지 않고 상당히 인상적인 속도 향상을 위해) PyPy의 번역 도구 체인 을 사용하여 RPython 호환 버전을 컴파일하거나 Cython 에서 확장 모듈을 빌드 할 수 있습니다. Cython 모듈은 거의 두 배 빠른 내 테스트에서 C 버전보다 빠릅니다 . 참고로 C 및 PyPy 벤치 마크 결과도 포함합니다.

C (로 컴파일 됨 gcc -O3 -lm)

% time ./euler12-c 
842161320

./euler12-c  11.95s 
 user 0.00s 
 system 99% 
 cpu 11.959 total

파이 파이 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파이썬 모듈로 다시 작성되었으며 , 일반적인 파이썬 파일에서 가져 와서 호출합니다. 는 _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
@SargeBorsch Cython 버전은 매우 최적화 된 C 소스로 컴파일되기 때문에 매우 빠르므로 C에서 그 성능을 얻을 수 있습니다.
Eli Korvigo

72

질문 3 : 요소를 결정하는 방식을 변경하지 않고 이러한 구현을 최적화하는 방법에 대한 힌트를 제공 할 수 있습니까? 어떤 방식 으로든 최적화 : 언어에 대해 더 좋고 빠르며 "네이티브"합니다.

C 구현은 차선책이며 (Thomas M. DuBuisson이 암시 한 바와 같이) 버전은 64 비트 정수 (예 : 데이터 유형)를 사용합니다. 나중에 어셈블리 목록을 조사 할 것이지만, 교육을받은 추측으로 컴파일 된 코드에서 일부 메모리 액세스가 진행되어 64 비트 정수 사용이 상당히 느려집니다. 그것은 코드이거나 생성 된 코드입니다 (SSE 레지스터에 64 비트 정수를 적게 넣거나 64 비트 정수로 반올림 할 수 있다는 사실이 느리다는 것입니다).

다음은 수정 된 코드입니다 ( 길이int로 간단히 바꾸고 명시 적으로 인라인 된 factorCount를 사용하지만 gcc -O3에서는 필요하다고 생각하지 않습니다).

#include <stdio.h>
#include <math.h>

static inline int factorCount(int n)
{
    double square = sqrt (n);
    int isquare = (int)square;
    int count = isquare == square ? -1 : 0;
    int candidate;
    for (candidate = 1; candidate <= isquare; candidate ++)
        if (0 == n % candidate) count += 2;
    return count;
}

int main ()
{
    int triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index++;
        triangle += index;
    }
    printf ("%d\n", triangle);
}

실행 + 타이밍 :

$ gcc -O3 -lm -o euler12 euler12.c; time ./euler12
842161320
./euler12  2.95s user 0.00s system 99% cpu 2.956 total

참고로 이전 답변에서 Thomas의 haskell 구현은 다음과 같습니다.

$ ghc -O2 -fllvm -fforce-recomp euler12.hs; time ./euler12                                                                                      [9:40]
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12 ...
842161320
./euler12  9.43s user 0.13s system 99% cpu 9.602 total

결론 : 훌륭한 컴파일러 인 ghc에서 아무것도 빼앗지 않지만 gcc는 일반적으로 더 빠른 코드를 생성합니다.


22
아주 좋아요! 비교를 위해 내 컴퓨터에서 C 솔루션이 실행되는 2.5 seconds동안 Haskell 코드와 유사한 수정 (Word32로 이동, INLINE pragma 추가)으로 런타임이 발생 4.8 seconds합니다. 아마도 무언가를 할 수 있습니다 (사소한 것이 아니라 보인다)-gcc 결과는 확실히 인상적입니다.
Thomas M. DuBuisson

1
감사! 아마도 문제는 실제 언어 자체가 아닌 다양한 컴파일러의 컴파일 된 출력 속도 일 것입니다. 다시 한 번 인텔 매뉴얼을 꺼내 수동으로 최적화하면 지식과 시간 (많은 지식이있는 경우)이 그대로 유지됩니다.
Raedwulf

56

이 블로그를 살펴보십시오 . 지난 1 년 동안 그는 Haskell과 Python에서 몇 가지 프로젝트 오일러 문제를 수행했으며 일반적으로 Haskell 이 훨씬 빠릅니다. 나는 그 언어들 사이에서 유창함과 코딩 스타일과 더 관련이 있다고 생각합니다.

파이썬 속도와 관련하여 잘못된 구현을 사용하고 있습니다! 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 "primes"를 확인한 것은 계산되지 않고 조회 만하는 사전 계산 된 소수의 방대한 목록 일뿐입니다. 물론 이것은 더 빠를 것이지만 Haskell 에서 소수 를 도출 하는 계산 속도에 대해서는 아무 것도 알려주지 않습니다 .
zxq9

21
@ zxq9 프라임 패키지 소스 ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… )에서 프라임 번호 목록이 있는 곳을 알려주세요 .
Fraser

4
소스가 프라임이 사전 계산되지 않았다는 것을 보여 주지만,이 속도는 완전히 미쳤고 C 버전보다 몇 마일 빠르므로 도대체 무슨 일이 벌어지고 있습니까?
세미콜론

1
@ 세미콜론 암기. 이 경우 Haskell은 런타임에 모든 소수를 기억하므로 각 반복마다 다시 계산할 필요가 없습니다.
Hauleth

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에서 long을 int로 변경하면 gcc -O3 -lm0.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 초가 걸립니다. 나는 관심있는 독자들에게 비슷한 속력을 남긴다.


3
비교를 위해 : 원본 c 버전 : 9.1690, thaumkid의 버전 : 0.1060 86x 개선.
thanos 2012 년

와. 하스켈은 추론 유형을 피하면 좋은 수행
Piyush Katariya

실제로는 그렇게 한 추론이 아닙니다. A) 유형 문제 및 유형 클래스 인스턴스 선택 문제를 디버그하거나 피할 수 있습니다. B) 몇 가지 최신 언어 확장으로 결정 불가능한 유형 문제를 디버그하고 피할 수 있습니다. 또한 프로그램을 구성 할 수 없게 만들어 개발 노력을 확장 할 수 없습니다.
codeshot

C 버전 0.11 s Intel skull canyon
코드 샷

13
질문 1 : 임의의 길이 정수를 사용하여 erlang, python 및 haskell 속도가 느리거나 값이 MAXINT보다 작지 않으면 속도가 느립니까?

가능성이 낮습니다. Erlang과 Haskell에 대해서는 많이 말할 수 없지만 (아래에 Haskell에 대해서는 약간), 파이썬에서 다른 많은 병목을 지적 할 수 있습니다. 프로그램이 파이썬에서 일부 값으로 연산을 실행하려고 시도 할 때마다 값이 올바른 유형인지 확인하고 약간의 시간이 소요됩니다. 귀하의 factorCount기능이 단지와 목록 할당 range (1, isquare + 1)다양한 시간 및 런타임을, malloc당신은 특히 C.에서와 -styled 메모리 할당 방법은이 카운터와 느린 반복하는 것보다 범위가 켜져 factorCount()여러 번 호출 때문에 목록을 많이 할당된다. 또한 파이썬이 해석되고 CPython 인터프리터가 최적화되는 데 중점을 두지 않는다는 것을 잊지 마십시오.

편집 : 아, 글쎄, 나는 파이썬 3을 사용하고 있으므로 range()목록을 반환하지 않고 생성기를 반환 한다는 것을 알았습니다 . 이 경우 목록 할당에 대한 나의 요점은 반 잘못입니다. 함수 range는 객체를 할당 합니다. 그럼에도 불구하고 비효율적이지만 많은 항목이있는 목록을 할당하는 것만 큼 비효율적이지는 않습니다.

질문 2 : 왜 하스켈이 그렇게 느립니까? 브레이크를 끄는 컴파일러 플래그가 있습니까, 아니면 내 구현입니까? (하스켈은 나에게 7 개의 인장이있는 책이기 때문에 후자는 상당히 가능성이 높다.)

Hugs 를 사용하고 있습니까? 포옹은 상당히 느린 통역사입니다. 당신이 그것을 사용하고 있다면, 아마도 당신은 GHC 로 더 나은 시간을 얻을 수 있습니다 -그러나 나는 단지 hypotosis를 코딩하고 있습니다.

질문 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

테일 재귀 여부를 결정하는 것은 컴파일러 / 통역사에 달려 있습니다. 예를 들어, 파이썬 인터프리터는 내가 잘 기억한다면 그것을하지 않습니다 (유창한 구문 때문에 유일하게 파이썬을 사용했습니다). 당신은 두 개의 매개 변수 (및 매개 변수 중 하나와 같은 이름을 가지고와 계승 함수로 이상한 물건을 발견하면 어쨌든 acc, accumulator등) 사람들이 그것을 왜 지금 당신이 알고 :)


@Hyperboreus 감사합니다! 또한 다음 질문에 정말 궁금합니다. 그러나 나는 나의 지식이 제한되어 있으므로 당신의 모든 질문에 대답 할 수는 없다고 경고합니다. 그것을 보상하려고 노력하면서 사람들이 더 쉽게 보완 할 수 있도록 답변 커뮤니티 위키를 만들었습니다.
brandizzi

범위 사용에 대해. 범위를 증분이있는 while 루프로 바꾸면 (C의 for 루프를 모방) 실행 시간이 실제로 두 배가됩니다. 발전기가 상당히 최적화 된 것 같습니다.
Hyperboreus

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 년 동안 개선 된 것 같습니다.

추신. 나는 그것이 오래된 질문이라는 것을 알고 있으며, 구글 검색에서 그것을 우연히 발견했습니다 (지금 검색중인 것을 잊어 버렸습니다 ...). LCO에 대한 질문에 대해 언급하고 하스켈에 대한 전반적인 느낌을 표현하고 싶었습니다. 최고 답변에 댓글을 달고 싶지만 댓글에는 코드 차단이 허용되지 않습니다.


9

C 버전에 대한 더 많은 숫자와 설명. 그 기간 동안 아무도 그렇게하지 않았습니다. 모든 사람이보고 배울 수 있도록 답을 올리십시오.

1 단계 : 저자의 프로그램 벤치 마크

노트북 사양 :

  • CPU i3 M380 (931MHz-최대 배터리 절약 모드)
  • 4GB 메모리
  • Win7 64 비트
  • Microsoft Visual Studio 2012 Ultimate
  • gcc가 포함 된 Cygwin 4.9.3
  • 파이썬 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

  • 정수형 은 현재 원래 프로그램과 동일합니다 (나중에 자세히 설명합니다)
  • 아키텍처 는 컴파일러 설정에 따라 x86 또는 x64입니다.
  • 컴파일러 는 gcc 또는 vs2012입니다

2 단계 : 다시 조사, 개선 및 벤치마킹

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

정수 유형은 다음 int long int32_t uint32_t int64_tuint64_t에서#include <stdint.h>

C에는 정수 유형의 LOTS와 함께 부호가 있거나 부호가없는 일부가 있으며 x86 또는 x64로 컴파일하는 옵션이 있습니다 (실제 정수 크기와 혼동하지 마십시오). 그것은 컴파일하고 실행할 많은 버전입니다 ^^

3 단계 : 숫자 이해

결정적인 결론 :

  • 32 비트 정수는 64 비트에 해당하는 것보다 ~ 200 % 빠릅니다.
  • 부호없는 64 비트 정수를 25 % 빠르게보다 서명 64 비트 (불행하게도, 나는 그것에 대해 아무런 설명이 없습니다)

속임수 질문 : "C에서 int와 long의 크기는 얼마입니까?"
정답은 : int와 long의 크기는 C에서 잘 정의되어 있지 않습니다!

C 스펙에서 :

int는 32 비트
이상이며 int 이상입니다.

gcc 매뉴얼 페이지 (-m32 및 -m64 플래그)에서 :

32 비트 환경은 int, long 및 32 비트에 대한 포인터를 설정하고 모든 i386 시스템에서 실행되는 코드를 생성합니다.
64 비트 환경은 int를 32 비트 및 long으로 설정하고 64 비트를 가리키는 AMD의 x86-64 아키텍처를위한 코드를 생성합니다.

MSDN 설명서 (데이터 유형 범위) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 바이트, 부호있는
long 으로도 알려져 있으며 , 4 바이트는 long int 및 부호있는 long으로도 알려져 있음

결론 : 학습 한 내용

  • 32 비트 정수는 64 비트 정수보다 빠릅니다.

  • 표준 정수 유형은 C 또는 C ++에서 잘 정의되지 않으며 컴파일러 및 아키텍처에 따라 다릅니다. 일관성과 예측 가능성이 필요한 경우의 uint32_t정수 제품군을 사용하십시오 #include <stdint.h>.

  • 속도 문제가 해결되었습니다. 다른 모든 언어는 수백 배나 뒤지고 C & C ++가 다시 승리합니다! 그들은 항상 그렇습니다. 다음 개선점은 OpenMP : D를 사용한 멀티 스레딩입니다.


호기심에서 인텔 컴파일러는 어떻게합니까? 그들은 일반적으로 숫자 코드를 최적화하는 데 능숙합니다.
kirbyfan64sos 2016 년

C 스펙이 "int is at 32 bits"를 보장한다고 언급 한 곳은 어디입니까? 내가 아는 유일한 보장은 INT_MININT_MAX(-32767 및 32767, int최소 16 비트 의 요구 사항을 실제로 부과합니다) 의 최소 ​​크기입니다 . long는 적어도 a만큼 커야 int하고, 범위 요건 평균 long은 32 비트 이상이다.
ShadowRanger


8

Erlang 구현을 살펴보십시오. 타이밍에는 전체 가상 머신의 시작, 프로그램 실행 및 가상 머신 정지가 포함되었습니다. erlang vm을 설정하고 중지하는 데 약간의 시간이 걸립니다.

erlang 가상 머신 자체에서 타이밍을 수행 한 경우 결과는 문제의 프로그램에 대해서만 실제 시간을 가지게됩니다. 그렇지 않으면 Erlang Vm을 시작하고로드하는 프로세스와 프로그램을 중지하는 데 걸리는 총 시간이 모두 프로그램에 넣는 총 시간에 포함된다고 생각합니다. 프로그램이 출력 중입니다. 가상 머신 내에서 프로그램의 시간을 정할 때 사용하는 얼랭 타이밍 자체를 사용하는 것이 좋습니다 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 초)과 비교할 때 충분히 작습니다.
RichardC

당신의 기계에! 우리는 당신이 태양 불 서버에서 일하고 있는지 여부를 모른다!. 시간은 기계 사양에 비례하는 변수이므로 고려되어야합니다.
Muzaaya Joshua

2
@RichardC Erlang이 더 빠르다고 언급 한 곳은 없습니다. :) 속도가 아니라 다른 목표를 가지고 있습니다!
예외 :

7

질문 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.370에 완료
Mark Washeim

당신의 결과 : 다른 76,576,500 사람 : 842,161,320 당신의 결과 뭔가 wronge입니다 THER
davidDavidson

다른 오일러 문제를 해결 했으므로 결과를 확인했습니다. projecteuler.net/problem=12에 대한 답변 은 76576500입니다. 이상하게 보이지만 방금 확인했습니다.
Mark Washeim

비교를 위해 Erlang 19를 Mark의 코드와 함께 사용하는 동안 원래 c 버전으로 9.03을 얻었으며 5.406, 167.0366 % 더 빠릅니다.
thanos 2012 년

5

C ++ (11), <나를 위해이 20ms - 실행 여기

귀하의 언어 별 지식을 향상시키는 데 도움이되는 팁을 원한다는 것을 이해하지만 여기에서 잘 다루어 졌으므로 귀하의 질문에 대한 수학 의견을 보았을 수도있는 사람들을위한 몇 가지 맥락을 추가 할 것이라고 생각했습니다. 코드가 너무 느 렸습니다.

이 답변은 주로 사람들이 질문 / 다른 답변의 코드를 더 쉽게 평가할 수 있도록 컨텍스트를 제공하기위한 것입니다.

이 코드는 다음을 기반으로 사용되는 언어와 관련이없는 몇 가지 (추악한) 최적화 만 사용합니다.

  1. 모든 traingle 번호는 n (n + 1) / 2 형식입니다.
  2. n과 n + 1은 coprime입니다
  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;
}

내 데스크톱의 경우 평균 19ms, 랩톱의 경우 80ms가 걸리며 여기에서 본 다른 코드와 크게 다릅니다. 그리고 의심 할 여지없이 많은 최적화가 여전히 가능합니다.


7
이것은 "어떻게도 4 개 언어에서 가능한 한 유사한 알고리즘을 구현하려고 노력했다"는 요청자가 요구 한 것이 아니다. 당신과 비슷한 많은 삭제 된 답변 중 하나에 대한 의견을 인용하려면 "언어에 관계없이 더 나은 알고리즘으로 더 빠른 속도를 얻을 수 있습니다."
Thomas M. DuBuisson

2
@ ThomasM.DuBuisson. 그것이 내가 얻는 것입니다. 질문 / 답변은 알고리즘 속도 향상이 중요하다는 것을 암시하지만 (물론 OP는 요구하지 않습니다) 명백한 예는 없습니다. 정확히 대답하지 않은 코드 인이 답변은 OP 코드가 얼마나 느리거나 빠른지 궁금한 사람과 같은 누군가에게 유용한 컨텍스트를 제공한다고 생각합니다.
user3125280

gcc는 많은 패턴을 미리 계산할 수 있습니다. int a = 0; for (int i = 0; i <10000000; ++ i) {a + = i;}는 컴파일 타임에 계산되므로 런타임시 <1ms가 걸립니다. 너무 계산
아서

@ 토마스 : 나는 user3125280에 동의해야합니다-언어는 바보 같은 일을 할 때 실제 프로그래밍 언어를 이길 수없는 방법 대신 똑똑한 일을하는 방법과 비교되어야합니다 . 스마트 알고리즘은 일반적으로 유연성, 사물을 연결 (결합)하는 능력 및 인프라에 관한 것보다 미세한 효율성에 대해 덜 관심을 갖습니다. 요점은 20ms 또는 50ms를 얻든 8 초 또는 8 분을 얻지 않든별로 중요 하지 않습니다 .
DarthGizka

5

시도 중 :

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 's c 버전 : 0.109 7916 %
Python3.6 : 54.795 16 %
pypy3.3-5.5-alpha : 13.291 65 %

그리고 다음 코드를 사용하여 얻었습니다.

원본 c 버전 : 8.629 100 %
thaumkid 's 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

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.79s 사용자 0.00s 시스템 99 % CPU 2.794 합계

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.