So sánh tốc độ với Project Euler: C vs Python vs Erlang vs Haskell


670

Tôi đã lấy Bài toán số 12 từ Project Euler làm bài tập lập trình và để so sánh các triển khai (chắc chắn không tối ưu) của tôi trong C, Python, Erlang và Haskell. Để có được thời gian thực hiện cao hơn, tôi tìm kiếm số tam giác đầu tiên với hơn 1000 ước thay vì 500 như đã nêu trong bài toán ban đầu.

Kết quả là như sau:

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

Con trăn

lorenzo@enzo:~/erlang$ time ./euler12.py 
842161320

real    1m16.632s
user    1m16.370s
sys 0m0.250s

Python với 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

Erlang:

lorenzo@enzo:~/erlang$ erlc euler12.erl 
lorenzo@enzo:~/erlang$ time erl -s euler12 solve
Erlang R13B03 (erts-5.7.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.4  (abort with ^G)
1> 842161320

real    0m48.259s
user    0m48.070s
sys 0m0.020s

Haskell:

lorenzo@enzo:~/erlang$ ghc euler12.hs -o euler12.hsx
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12.hsx ...
lorenzo@enzo:~/erlang$ time ./euler12.hsx 
842161320

real    2m37.326s
user    2m37.240s
sys 0m0.080s

Tóm lược:

  • C: 100%
  • Python: 692% (118% với PyPy)
  • Erlang: 436% (135% nhờ RichardC)
  • Haskell: 1421%

Tôi cho rằng C có lợi thế lớn vì nó sử dụng dài cho các phép tính và không phải là số nguyên độ dài tùy ý như ba số còn lại. Ngoài ra, nó không cần tải thời gian chạy trước (Làm những cái khác?).

Câu hỏi 1: Erlang, Python và Haskell có bị mất tốc độ do sử dụng các số nguyên độ dài tùy ý hay không miễn là các giá trị nhỏ hơn MAXINT?

Câu 2: Tại sao Haskell lại chậm như vậy? Có một cờ biên dịch tắt phanh hay là do tôi thực hiện? (Cuốn thứ hai khá có thể xảy ra vì Haskell là một cuốn sách có bảy phong ấn đối với tôi.)

Câu hỏi 3: Bạn có thể cung cấp cho tôi một số gợi ý về cách tối ưu hóa các triển khai này mà không thay đổi cách tôi xác định các yếu tố không? Tối ưu hóa theo bất kỳ cách nào: đẹp hơn, nhanh hơn, "bản địa" hơn với ngôn ngữ.

BIÊN TẬP:

Câu hỏi 4: Việc triển khai chức năng của tôi có cho phép LCO (tối ưu hóa cuộc gọi cuối cùng hay còn gọi là loại bỏ đệ quy đuôi) và do đó tránh thêm các khung không cần thiết vào ngăn xếp cuộc gọi không?

Tôi thực sự đã cố gắng thực hiện cùng một thuật toán giống nhau nhất có thể trong bốn ngôn ngữ, mặc dù tôi phải thừa nhận rằng kiến ​​thức Haskell và Erlang của tôi rất hạn chế.


Mã nguồn được sử dụng:

#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 (và Seth) Không thực sự là C nhanh hay tuyệt vời, nhưng nó được coi là dễ viết mã trình diễn (điều đó có thể không đúng, nhưng hầu hết các chương trình dường như có thể, rất đủ đúng). Khi tôi khám phá trong câu trả lời của mình và đã thấy đúng theo thời gian, kỹ năng lập trình và kiến ​​thức về tối ưu hóa phổ biến cho ngôn ngữ đã chọn là rất quan trọng (đặc biệt là đối với Haskell).
Thomas M. DuBuisson

52
Chỉ cần kiểm tra với Mathematica - phải mất 0.25sec (với C phải mất 6sec đây), và mã chỉ là: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. tiếng hoan hô!
tsvikas

35
Có ai khác ngoài đó nhớ những cuộc chiến giữa C và lắp ráp không? "Chắc chắn rồi! Bạn có thể viết mã của mình nhanh hơn gấp 10 lần bằng C, nhưng mã C của bạn có thể chạy nhanh như vậy không? ..." Tôi chắc chắn rằng các trận chiến tương tự đã được chiến đấu giữa mã máy và lắp ráp.
JS.

39
@JS: Có lẽ là không, vì lắp ráp chỉ đơn giản là một tập hợp các ký hiệu mà bạn nhập thay vì mã máy nhị phân thô - thông thường có sự tương ứng 1-1 giữa chúng.
Callum Rogers

9
kết luận, đối với Haskell: -O2 cho tốc độ tăng gấp 3 lần và sử dụng Int thay vì Integer khoảng 4x-6x cho tổng tốc độ tăng từ 12x-14x trở lên.
Will Ness

Câu trả lời:


794

Sử dụng GHC 7.0.3, gcc 4.4.6, Linux 2.6.29trên một Core2 Duo máy (2.5GHz) x86_64, biên soạn sử dụng ghc -O2 -fllvm -fforce-recompcho Haskell và gcc -O3 -lmC.

  • Thói quen C của bạn chạy trong 8.4 giây (nhanh hơn tốc độ của bạn có thể là do -O3)
  • Giải pháp Haskell chạy trong 36 giây (do -O2cờ)
  • factorCount'Mã của bạn không được gõ rõ ràng và mặc định Integer(cảm ơn Daniel vì đã sửa lỗi chẩn đoán sai của tôi ở đây!). Đưa ra một chữ ký loại rõ ràng (dù sao cũng là tiêu chuẩn thực hành) Intvà thời gian thay đổi thành 11,1 giây
  • trong factorCount'bạn đã gọi không cần thiết fromIntegral. Một sửa chữa kết quả không có thay đổi mặc dù (trình biên dịch là thông minh, may mắn cho bạn).
  • Bạn đã sử dụng modở đâu remlà nhanh hơn và đủ. Điều này thay đổi thời gian thành 8,5 giây .
  • factorCount'liên tục áp dụng hai đối số phụ không bao giờ thay đổi ( number, sqrt). Một chuyển đổi worker / Wrapper cung cấp cho chúng ta:
 $ time ./so
 842161320  

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

Đúng vậy, 7,95 giây . Luôn nửa giây nhanh hơn so với các giải pháp C . Không có -fllvmcờ tôi vẫn nhận được 8.182 seconds, vì vậy phụ trợ NCG ​​cũng hoạt động tốt trong trường hợp này.

Kết luận: Haskell là tuyệt vời.

Mã kết quả

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

EDIT: Vì vậy, bây giờ chúng tôi đã khám phá điều đó, hãy giải quyết các câu hỏi

Câu hỏi 1: Do erlang, python và haskell có bị mất tốc độ do sử dụng các số nguyên có độ dài tùy ý hay không miễn là các giá trị nhỏ hơn MAXINT?

Trong Haskell, việc sử dụng Integerchậm hơn Intnhưng chậm hơn bao nhiêu tùy thuộc vào các tính toán được thực hiện. May mắn thay (đối với máy 64 bit) Intlà đủ. Vì tính di động, có lẽ bạn nên viết lại mã của tôi để sử dụng Int64hoặc Word64(C không phải là ngôn ngữ duy nhất có a long).

Câu 2: Tại sao haskell lại chậm như vậy? Có một cờ biên dịch tắt phanh hay là do tôi thực hiện? (Cuốn thứ hai khá có thể xảy ra vì haskell là một cuốn sách có bảy phong ấn đối với tôi.)

Câu hỏi 3: Bạn có thể cung cấp cho tôi một số gợi ý về cách tối ưu hóa các triển khai này mà không thay đổi cách tôi xác định các yếu tố không? Tối ưu hóa theo bất kỳ cách nào: đẹp hơn, nhanh hơn, "bản địa" hơn với ngôn ngữ.

Đó là những gì tôi đã trả lời ở trên. Câu trả lời là

  • 0) Sử dụng tối ưu hóa thông qua -O2
  • 1) Sử dụng các loại nhanh (đáng chú ý: không có hộp thư) khi có thể
  • 2) remkhông mod(tối ưu hóa thường xuyên bị lãng quên) và
  • 3) chuyển đổi worker / Wrapper (có lẽ là tối ưu hóa phổ biến nhất).

Câu hỏi 4: Việc triển khai chức năng của tôi có cho phép LCO và do đó tránh thêm các khung không cần thiết vào ngăn xếp cuộc gọi không?

Vâng, đó không phải là vấn đề. Tốt công việc và vui mừng bạn xem xét điều này.


25
@Karl Bởi vì remthực sự là một thành phần phụ của modhoạt động (chúng không giống nhau). Nếu bạn tìm trong thư viện GHC Base, bạn sẽ thấy modcác bài kiểm tra cho một số điều kiện và điều chỉnh dấu hiệu cho phù hợp. (xem modInt#trong Base.lhs)
Thomas M. DuBuisson

20
Một điểm dữ liệu khác: Tôi đã viết một bản dịch nhanh Haskell của chương trình C mà không cần xem Haskell của @ Hyperboreus. Vì vậy, đó là một chút gần gũi hơn với tiêu chuẩn thành ngữ Haskell, và tối ưu hóa duy nhất tôi thêm cố tình được thay thế modvới remsau khi đọc câu trả lời này (heh, oops). Xem liên kết về thời gian của tôi, nhưng phiên bản ngắn "gần giống với C".
CA McCann

106
Thậm chí nghĩ rằng phiên bản C chạy nhanh hơn trên máy của tôi, bây giờ tôi có một sự tôn trọng mới đối với Haskell. +1
Seth Carnegie

11
Điều này khá ngạc nhiên với tôi, mặc dù tôi chưa thử nó. Vì bản gốc factorCount'là đệ quy đuôi nên tôi nghĩ trình biên dịch có thể phát hiện ra các tham số bổ sung không bị thay đổi và tối ưu hóa đệ quy đuôi chỉ cho các tham số thay đổi (xét cho cùng, Haskell là ngôn ngữ thuần túy, điều này rất dễ). Bất cứ ai nghĩ rằng trình biên dịch có thể làm điều đó hoặc tôi nên quay lại để đọc thêm các bài lý thuyết?
kizzx2

22
@ kizzx2: Có một vé GHC để thêm nó. Từ những gì tôi đã hiểu, sự chuyển đổi này có thể dẫn đến việc phân bổ bổ sung các đối tượng đóng. Điều này có nghĩa là hiệu suất kém hơn trong một số trường hợp, nhưng như Johan Tibell gợi ý trong bài đăng trên blog của mình, điều này có thể tránh được nếu trình bao bọc kết quả có thể được nội tuyến.
hammar

224

Có một số vấn đề với việc thực hiện Erlang. Là cơ sở cho những điều sau đây, thời gian thực hiện được đo của tôi cho chương trình Erlang chưa sửa đổi của bạn là 47,6 giây, so với 12,7 giây cho mã C.

Điều đầu tiên bạn nên làm nếu bạn muốn chạy mã Erlang tính toán chuyên sâu là sử dụng mã gốc. Biên dịch với erlc +native euler12thời gian xuống còn 41,3 giây. Tuy nhiên, đây là một sự tăng tốc thấp hơn nhiều (chỉ 15%) so với dự kiến ​​từ quá trình biên dịch riêng trên loại mã này và vấn đề là việc bạn sử dụng -compile(export_all). Điều này rất hữu ích cho thử nghiệm, nhưng thực tế là tất cả các chức năng có khả năng tiếp cận từ bên ngoài khiến trình biên dịch gốc rất bảo thủ. (Trình giả lập BEAM bình thường không bị ảnh hưởng nhiều.) Thay thế khai báo này bằng -export([solve/0]).cách tăng tốc tốt hơn nhiều: 31,5 giây (gần 35% so với đường cơ sở).

Nhưng bản thân mã có một vấn đề: cho mỗi lần lặp trong vòng lặp FactCount, bạn thực hiện kiểm tra này:

factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;

Mã C không làm điều này. Nói chung, có thể khó để so sánh công bằng giữa các triển khai khác nhau của cùng một mã và đặc biệt nếu thuật toán là số, bởi vì bạn cần chắc chắn rằng chúng thực sự đang làm cùng một việc. Một lỗi làm tròn nhẹ trong một lần thực hiện do một số lỗi chính tả ở đâu đó có thể khiến nó thực hiện nhiều lần lặp hơn lần khác mặc dù cả hai cuối cùng đều đạt được cùng một kết quả.

Để loại bỏ nguồn lỗi có thể này (và loại bỏ kiểm tra bổ sung trong mỗi lần lặp), tôi viết lại hàm FactCount như sau, được mô hình chặt chẽ trên mã 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.

Việc viết lại này, không export_all, và biên dịch riêng, đã cho tôi thời gian chạy sau:

$ erlc +native euler12.erl
$ time erl -noshell -s euler12 solve
842161320

real    0m19.468s
user    0m19.450s
sys 0m0.010s

không quá tệ so với mã C:

$ time ./a.out 
842161320

real    0m12.755s
user    0m12.730s
sys 0m0.020s

xem xét rằng Erlang hoàn toàn không hướng đến việc viết mã số, chỉ chậm hơn 50% so với C trên một chương trình như thế này là khá tốt.

Cuối cùng, liên quan đến câu hỏi của bạn:

Câu hỏi 1: Do erlang, python và haskell có tốc độ lỏng do sử dụng các số nguyên có độ dài tùy ý hay không miễn là các giá trị nhỏ hơn MAXINT?

Vâng, phần nào. Trong Erlang, không có cách nào để nói "sử dụng số học 32/64-bit với sự bao bọc", vì vậy trừ khi trình biên dịch có thể chứng minh một số giới hạn trên số nguyên của bạn (và thường không thể), nó phải kiểm tra tất cả các tính toán để xem nếu chúng có thể phù hợp với một từ được gắn thẻ duy nhất hoặc nếu nó phải biến chúng thành các bignums phân bổ heap. Ngay cả khi không có bignums nào được sử dụng trong thực tế khi chạy, những kiểm tra này sẽ phải được thực hiện. Mặt khác, điều đó có nghĩa là bạn biết rằng thuật toán sẽ không bao giờ thất bại vì một số nguyên bất ngờ nếu bạn đột nhiên cung cấp cho nó đầu vào lớn hơn trước.

Câu hỏi 4: Việc triển khai chức năng của tôi có cho phép LCO và do đó tránh thêm các khung không cần thiết vào ngăn xếp cuộc gọi không?

Có, mã Erlang của bạn là chính xác đối với tối ưu hóa cuộc gọi cuối cùng.


2
Tôi đồng ý với bạn. Điểm chuẩn này không chính xác đặc biệt đối với Erlang vì một số lý do
Muzaaya Joshua

156

Liên quan đến tối ưu hóa Python, ngoài việc sử dụng PyPy (để tăng tốc khá ấn tượng với thay đổi bằng 0 cho mã của bạn), bạn có thể sử dụng chuỗi công cụ dịch thuật của PyPy để biên dịch phiên bản tuân thủ RPython hoặc Cython để xây dựng mô-đun mở rộng, cả hai nhanh hơn phiên bản C trong thử nghiệm của tôi, với mô-đun Cython nhanh gần gấp đôi . Để tham khảo tôi cũng bao gồm kết quả điểm chuẩn C và PyPy:

C (biên dịch với 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 (sử dụng bản sửa đổi PyPy mới nhất, c2f583445aee)

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

Con trăn 0,15

% time python euler12-cython.py
842161320
python euler12-cython.py  
6.27s user 0.00s 
system 99% 
cpu 6.274 total

Phiên bản RPython có một vài thay đổi quan trọng. Để dịch thành một chương trình độc lập, bạn cần xác định target, trong trường hợp này là mainhàm. Dự kiến ​​sẽ chấp nhận sys.argvvì đây chỉ là đối số và được yêu cầu trả về một int. Bạn có thể dịch nó bằng cách sử dụng translate.py, % translate.py euler12-rpython.pydịch sang C và biên dịch nó cho bạn.

# 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

Phiên bản Cython được viết lại dưới dạng một mô-đun mở rộng _euler12.pyx, mà tôi nhập và gọi từ một tệp python bình thường. Về _euler12.pyxcơ bản giống như phiên bản của bạn, với một số khai báo kiểu tĩnh bổ sung. Setup.py có bản soạn sẵn bình thường để xây dựng phần mở rộng, sử dụng 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
)

Tôi thực sự có rất ít kinh nghiệm với RPython hoặc Cython, và rất ngạc nhiên với kết quả. Nếu bạn đang sử dụng CPython, việc viết các đoạn mã thâm dụng CPU của bạn trong mô-đun mở rộng Cython có vẻ như là một cách thực sự dễ dàng để tối ưu hóa chương trình của bạn.


6
Tôi tò mò, phiên bản C có thể được tối ưu hóa nhanh nhất bằng CPython không?
Tên hiển thị

4
@SargeBorsch rằng phiên bản Cython rất nhanh, vì nó biên dịch thành nguồn C được tối ưu hóa cao, có nghĩa là bạn chắc chắn có thể lấy hiệu suất đó ra khỏi C.
Eli Korvigo

72

Câu hỏi 3: Bạn có thể cung cấp cho tôi một số gợi ý về cách tối ưu hóa các triển khai này mà không thay đổi cách tôi xác định các yếu tố không? Tối ưu hóa theo bất kỳ cách nào: đẹp hơn, nhanh hơn, "bản địa" hơn với ngôn ngữ.

Việc triển khai C là tối ưu (theo gợi ý của Thomas M. DuBuisson), phiên bản sử dụng số nguyên 64 bit (tức là kiểu dữ liệu dài ). Tôi sẽ điều tra danh sách lắp ráp sau, nhưng với phỏng đoán có giáo dục, có một số truy cập bộ nhớ đang diễn ra trong mã được biên dịch, khiến việc sử dụng số nguyên 64 bit chậm hơn đáng kể. Đó là hoặc mã được tạo (có thể là bạn có thể điều chỉnh các số nguyên 64 bit trong một thanh ghi SSE hoặc làm tròn số nhân thành số nguyên 64 bit chậm hơn).

Đây là mã được sửa đổi (chỉ đơn giản là thay thế dài bằng int và tôi đã in rõ ràng FactCount, mặc dù tôi không nghĩ rằng điều này là cần thiết với 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);
}

Chạy + thời gian nó cung cấp:

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

Để tham khảo, việc triển khai haskell của Thomas trong câu trả lời trước đó đưa ra:

$ 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

Kết luận: Không lấy gì từ ghc, nó là một trình biên dịch tuyệt vời, nhưng gcc thường tạo mã nhanh hơn.


22
Rất đẹp! Để so sánh, trên máy của tôi, giải pháp C của bạn chạy trong 2.5 secondskhi một sửa đổi tương tự với mã Haskell (chuyển sang Word32, thêm pragma INLINE) dẫn đến thời gian chạy 4.8 seconds. Có lẽ một cái gì đó có thể được thực hiện (không phải tầm thường, dường như) - kết quả gcc chắc chắn là ấn tượng.
Thomas M. DuBuisson

1
Cảm ơn! Có lẽ câu hỏi nên là tốc độ đầu ra được biên dịch bởi các trình biên dịch khác nhau chứ không phải là ngôn ngữ thực tế. Sau đó, một lần nữa, rút ​​ra các hướng dẫn sử dụng Intel và tối ưu hóa bằng tay vẫn sẽ giành chiến thắng hoàn toàn (miễn là bạn có kiến ​​thức và thời gian (rất nhiều)).
Raedwulf

56

Hãy xem blog này . Trong khoảng một năm qua, anh ấy đã thực hiện một số vấn đề về Project Euler trong Haskell và Python, và anh ấy thường thấy Haskell nhanh hơn nhiều. Tôi nghĩ rằng giữa những ngôn ngữ đó có liên quan nhiều hơn đến sự trôi chảy và phong cách mã hóa của bạn.

Khi nói đến tốc độ Python, bạn đang sử dụng triển khai sai! Hãy thử PyPy , và đối với những thứ như thế này, bạn sẽ thấy nó nhanh hơn nhiều.


32

Việc triển khai Haskell của bạn có thể được tăng tốc đáng kể bằng cách sử dụng một số chức năng từ các gói Haskell. Trong trường hợp này, tôi đã sử dụng các số nguyên tố, chỉ được cài đặt với 'số nguyên tố cài đặt cabal';)

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)

Thời gian:

Chương trình ban đầu của bạn:

PS> measure-command { bin\012_slow.exe }

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Cải thiện thực hiện

PS> measure-command { bin\012.exe }

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Như bạn có thể thấy, cái này chạy trong 38 mili giây trên cùng một máy mà máy của bạn chạy trong 16 giây :)

Các lệnh biên dịch:

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

5
Lần cuối tôi kiểm tra "số nguyên tố" của Haskell chỉ là một danh sách khổng lồ các số nguyên tố được tính toán trước - không tính toán, chỉ cần tra cứu. Vì vậy, có, tất nhiên điều này sẽ nhanh hơn, nhưng nó sẽ cho bạn biết gì về tốc độ tính toán của phát sinh số nguyên tố trong Haskell.
zxq9

21
@ zxq9 bạn có thể chỉ cho tôi nơi trong nguồn gói số nguyên tố ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/ Lỗi ) danh sách các số nguyên tố được đặt ở đâu không?
Fraser

4
Trong khi nguồn cho thấy các số nguyên tố không được tính toán trước, tốc độ tăng tốc này hoàn toàn điên rồ, nhanh hơn nhiều so với phiên bản C, vậy cái quái gì đang diễn ra?
dấu chấm phẩy

1
@semiaolon ghi nhớ. Trong trường hợp này, tôi nghĩ rằng Haskell đã ghi nhớ tất cả các số nguyên tố trong thời gian chạy, do đó không cần phải tính toán lại chúng mỗi lần lặp.
Hauleth 30/03/2016

5
Đó là 1000 ước, không phải 500.
Casper Færgemand

29

Chỉ để cho vui. Sau đây là cách triển khai Haskell 'bản địa' hơn:

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

Sử dụng ghc -O3 , điều này luôn chạy trong 0,55-0,58 giây trên máy của tôi (Core i7 1,73GHz).

Hàm FactCount hiệu quả hơn cho phiên bản C:

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;
}

Thay đổi độ dài thành ints trong main, bằng cách sử dụng gcc -O3 -lm, điều này luôn chạy trong 0,31-0,35 giây.

Cả hai đều có thể được thực hiện để chạy nhanh hơn nữa nếu bạn tận dụng thực tế là số tam giác thứ n = n * (n + 1) / 2, và n và (n + 1) có các thừa số nguyên tố hoàn toàn khác nhau, do đó số lượng các yếu tố của mỗi nửa có thể được nhân lên để tìm số lượng các yếu tố của tổng thể. Sau đây là

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);
}

sẽ giảm thời gian chạy mã c xuống 0,17-0,19 giây và nó có thể xử lý các tìm kiếm lớn hơn nhiều - hơn 10000 yếu tố mất khoảng 43 giây trên máy của tôi. Tôi để lại một speedk haskell tương tự cho người đọc quan tâm.


3
Chỉ để so sánh: phiên bản c gốc: 9.1690, phiên bản thaumkid: cải thiện 0.1060 86x.
thanos

Ồ Haskell thực hiện tuyệt vời khi bạn tránh các loại suy luận
Piyush Katariya

Trên thực tế, đó không phải là suy luận đã làm điều đó. Điều đó chỉ giúp bạn A) gỡ lỗi hoặc tránh các vấn đề về kiểu và các vấn đề lựa chọn cá thể kiểu chữ B) gỡ lỗi và tránh một số vấn đề về loại không thể giải quyết được với một vài phần mở rộng ngôn ngữ hiện đại. Nó cũng giúp bạn làm cho các chương trình của bạn không thể hoàn thành để bạn không bao giờ có thể mở rộng quy mô nỗ lực phát triển của mình.
codeshot

c phiên bản 0.11 s trên hẻm núi Intel
codeshot

13
Câu hỏi 1: Do erlang, python và haskell có tốc độ lỏng do sử dụng các số nguyên có độ dài tùy ý hay không miễn là các giá trị nhỏ hơn MAXINT?

Điều này là không thể. Tôi không thể nói nhiều về Erlang và Haskell (tốt, có thể một chút về Haskell bên dưới) nhưng tôi có thể chỉ ra rất nhiều nút thắt khác trong Python. Mỗi khi chương trình cố gắng thực hiện một thao tác với một số giá trị trong Python, nó sẽ xác minh xem các giá trị đó có thuộc loại thích hợp hay không và nó sẽ tốn một chút thời gian. factorCountHàm của bạn chỉ phân bổ một danh sách với range (1, isquare + 1)nhiều thời gian khác nhau và thời gian chạy, mallocphân bổ bộ nhớ được tăng tốc chậm hơn so với việc lặp lại trên một phạm vi với bộ đếm như bạn làm trong C. Đáng chú ý, factorCount()được gọi là nhiều lần và do đó phân bổ rất nhiều danh sách. Ngoài ra, chúng ta đừng quên rằng Python được thông dịch và trình thông dịch CPython không tập trung lớn vào việc được tối ưu hóa.

EDIT : oh, tốt, tôi lưu ý rằng bạn đang sử dụng Python 3 vì vậy range()không trả về danh sách, mà là một trình tạo. Trong trường hợp này, quan điểm của tôi về việc phân bổ danh sách là sai một nửa: hàm chỉ phân bổ rangecác đối tượng, tuy nhiên không hiệu quả nhưng không hiệu quả bằng việc phân bổ danh sách với nhiều mục.

Câu 2: Tại sao haskell lại chậm như vậy? Có một cờ biên dịch tắt phanh hay là do tôi thực hiện? (Cuốn thứ hai khá có thể xảy ra vì haskell là một cuốn sách có bảy phong ấn đối với tôi.)

Bạn đang sử dụng Hugs ? Hugs là một thông dịch viên chậm đáng kể. Nếu bạn đang sử dụng nó, có lẽ bạn có thể có được thời gian tốt hơn với GHC - nhưng tôi chỉ là những người bị thôi miên, loại công cụ mà trình biên dịch Haskell tốt thực hiện dưới mui xe khá hấp dẫn và vượt quá tầm hiểu biết của tôi :)

Câu hỏi 3: Bạn có thể cung cấp cho tôi một số gợi ý về cách tối ưu hóa các triển khai này mà không thay đổi cách tôi xác định các yếu tố không? Tối ưu hóa theo bất kỳ cách nào: đẹp hơn, nhanh hơn, "bản địa" hơn với ngôn ngữ.

Tôi muốn nói rằng bạn đang chơi một trò chơi không vui. Phần tốt nhất để biết các ngôn ngữ khác nhau là sử dụng chúng theo cách khác nhau nhất có thể :) Nhưng tôi lạc đề, tôi chỉ không có bất kỳ khuyến nghị nào cho điểm này. Xin lỗi, tôi hy vọng ai đó có thể giúp bạn trong trường hợp này :)

Câu hỏi 4: Việc triển khai chức năng của tôi có cho phép LCO và do đó tránh thêm các khung không cần thiết vào ngăn xếp cuộc gọi không?

Theo tôi nhớ, bạn chỉ cần đảm bảo rằng cuộc gọi đệ quy của bạn là lệnh cuối cùng trước khi trả về một giá trị. Nói cách khác, một chức năng như chức năng dưới đây có thể sử dụng tối ưu hóa như vậy:

def factorial(n, acc=1):
    if n > 1:
        acc = acc * n
        n = n - 1
        return factorial(n, acc)
    else:
        return acc

Tuy nhiên, bạn sẽ không có tối ưu hóa như vậy nếu chức năng của bạn là như dưới đây, bởi vì có một hoạt động (nhân) sau cuộc gọi đệ quy:

def factorial2(n):
    if n > 1:
        f = factorial2(n-1)
        return f*n
    else:
        return 1

Tôi đã tách các hoạt động trong một số biến cục bộ để làm rõ các hoạt động nào được thực hiện. Tuy nhiên, thông thường nhất là để xem các chức năng này như dưới đây, nhưng chúng tương đương với điểm tôi đang thực hiện:

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

Lưu ý rằng tùy thuộc vào trình biên dịch / trình thông dịch để quyết định xem nó có thực hiện đệ quy đuôi hay không. Ví dụ, trình thông dịch Python không làm điều đó nếu tôi nhớ rõ (tôi đã sử dụng Python trong ví dụ của mình chỉ vì cú pháp lưu loát của nó). Dù sao, nếu bạn tìm thấy những thứ lạ như các hàm giai thừa với hai tham số (và một trong các tham số có tên như acc, accumulatorv.v.) bây giờ bạn biết tại sao mọi người làm điều đó :)


@Hyperboreus cảm ơn bạn! Ngoài ra, tôi thực sự tò mò về câu hỏi tiếp theo của bạn. Tuy nhiên, tôi cảnh báo bạn rằng kiến ​​thức của tôi còn hạn chế nên tôi không thể trả lời mọi câu hỏi của bạn. Để cố gắng bù đắp, tôi đã tạo ra wiki cộng đồng trả lời của mình để mọi người có thể dễ dàng bổ sung hơn.
brandizzi

Về việc sử dụng phạm vi. Khi tôi thay thế phạm vi bằng một vòng lặp while bằng gia số (bắt chước vòng lặp for của C), thời gian thực hiện thực sự tăng gấp đôi. Tôi đoán máy phát điện khá tối ưu.
Hyperboreus

12

Với Haskell, bạn thực sự không cần phải suy nghĩ rõ ràng về việc thu hồi.

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

Trong đoạn mã trên, tôi đã thay thế các truy vấn rõ ràng trong câu trả lời của @Thomas bằng các hoạt động danh sách phổ biến. Mã vẫn thực hiện chính xác điều tương tự mà không cần chúng tôi lo lắng về đệ quy đuôi. Nó chạy (~ 7,49s ) chậm hơn khoảng 6% so với phiên bản trong câu trả lời của @Thomas (~ 7.04s ) trên máy của tôi với GHC 7.6.2, trong khi phiên bản C từ @Raedwulf chạy ~ 3.15s . Có vẻ như GHC đã được cải thiện trong năm qua.

Tái bút Tôi biết đó là một câu hỏi cũ và tôi vấp phải nó từ các tìm kiếm của google (tôi đã quên những gì tôi đang tìm kiếm, bây giờ ...). Chỉ muốn bình luận về câu hỏi về LCO và nói lên cảm xúc của tôi về Haskell nói chung. Tôi muốn bình luận về câu trả lời hàng đầu, nhưng ý kiến ​​không cho phép khối mã.


9

Một số số và giải thích thêm cho phiên bản C. Rõ ràng không ai làm điều đó trong suốt những năm đó. Hãy nhớ upvote câu trả lời này để nó có thể lên đầu cho mọi người xem và học hỏi.

Bước một: Điểm chuẩn của các chương trình của tác giả

Thông số kỹ thuật của máy tính xách tay:

  • CPU i3 M380 (931 MHz - chế độ tiết kiệm pin tối đa)
  • Bộ nhớ 4GB
  • Win7 64 bit
  • Microsoft Visual Studio 2012 Cuối cùng
  • Cygwin với gcc 4.9.3
  • Python 2.7.10

Các lệnh:

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

Tên tập tin là: integertype_architecture_compiler.exe

  • integertype là giống như các chương trình ban đầu cho bây giờ (thêm về điều này sau)
  • kiến trúc là x86 hoặc x64 tùy thuộc vào cài đặt trình biên dịch
  • trình biên dịch là gcc hoặc vs2012

Bước hai: Điều tra, cải thiện và điểm chuẩn một lần nữa

VS nhanh hơn 250% so với gcc. Hai trình biên dịch sẽ cho một tốc độ tương tự. Rõ ràng, có gì đó không đúng với mã hoặc các tùy chọn trình biên dịch. Hãy điều tra!

Điểm quan tâm đầu tiên là các loại số nguyên. Chuyển đổi có thể tốn kém và tính nhất quán là quan trọng để tạo và tối ưu hóa mã tốt hơn. Tất cả các số nguyên phải cùng loại.

Đó là một mớ hỗn độn của intlongngay bây giờ. Chúng tôi sẽ cải thiện điều đó. Sử dụng loại nào? Nhanh nhất. Phải điểm chuẩn cho họ!

----------
$ time ./int_x86_vs2012.exe

real    0m8.440s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int_x64_vs2012.exe

real    0m8.408s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int32_x86_vs2012.exe

real    0m8.408s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int32_x64_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x86_vs2012.exe

real    0m18.112s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x64_vs2012.exe

real    0m18.611s
user    0m0.000s
sys     0m0.015s
----------
$ time ./long_x86_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.000s
----------
$ time ./long_x64_vs2012.exe

real    0m8.440s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x86_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x64_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.015s
----------
$ time ./uint64_x86_vs2012.exe

real    0m15.428s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint64_x64_vs2012.exe

real    0m15.725s
user    0m0.015s
sys     0m0.015s
----------
$ time ./int_x64_gcc.exe

real    0m8.531s
user    0m8.329s
sys     0m0.015s
----------
$ time ./int32_x64_gcc.exe

real    0m8.471s
user    0m8.345s
sys     0m0.000s
----------
$ time ./int64_x64_gcc.exe

real    0m20.264s
user    0m20.186s
sys     0m0.015s
----------
$ time ./long_x64_gcc.exe

real    0m20.935s
user    0m20.809s
sys     0m0.015s
----------
$ time ./uint32_x64_gcc.exe

real    0m8.393s
user    0m8.346s
sys     0m0.015s
----------
$ time ./uint64_x64_gcc.exe

real    0m16.973s
user    0m16.879s
sys     0m0.030s

Các kiểu số nguyên là int long int32_t uint32_t int64_tuint64_ttừ#include <stdint.h>

Có rất nhiều loại số nguyên trong C, cộng với một số ký / không dấu để chơi, cộng với lựa chọn biên dịch là x86 hoặc x64 (không bị nhầm lẫn với kích thước số nguyên thực tế). Đó là rất nhiều phiên bản để biên dịch và chạy ^^

Bước ba: Hiểu các con số

Kết luận dứt khoát:

  • Số nguyên 32 bit nhanh hơn ~ 200% so với tương đương 64 bit
  • số nguyên 64 bit không dấu nhanh hơn 25% so với 64 bit đã ký (Thật không may, tôi không có lời giải thích nào cho điều đó)

Câu hỏi lừa: "Kích thước của int và long trong C là bao nhiêu?"
Câu trả lời đúng là: Kích thước của int và long trong C không được xác định rõ!

Từ thông số kỹ thuật C:

int dài ít nhất 32 bit
là ít nhất một int

Từ trang gcc man (cờ -m32 và -m64):

Môi trường 32 bit đặt int, long và con trỏ thành 32 bit và tạo mã chạy trên bất kỳ hệ thống i386 nào.
Môi trường 64 bit đặt int thành 32 bit, dài và con trỏ thành 64 bit và tạo mã cho kiến ​​trúc x86-64 của AMD.

Từ tài liệu MSDN (Phạm vi loại dữ liệu) https://msdn.microsoft.com/en-us/l Library / s3f49ktz% 28v = vs.110% 29.aspx :

int, 4 byte, cũng được biết là đã ký
dài, 4 byte, cũng biết là int dài và ký dài int

Để kết luận điều này: Bài học kinh nghiệm

  • Số nguyên 32 bit nhanh hơn số nguyên 64 bit.

  • Các loại số nguyên tiêu chuẩn không được xác định rõ trong C và C ++, chúng khác nhau tùy thuộc vào trình biên dịch và kiến ​​trúc. Khi bạn cần sự nhất quán và dự đoán, hãy sử dụng họ uint32_tsố nguyên từ #include <stdint.h>.

  • Vấn đề tốc độ được giải quyết. Tất cả các ngôn ngữ khác đã trở lại hàng trăm phần trăm phía sau, C & C ++ giành chiến thắng một lần nữa! Họ luôn làm thế. Cải tiến tiếp theo sẽ là đa luồng sử dụng OpenMP: D


Vì tò mò, các trình biên dịch Intel làm như thế nào? Họ thường thực sự giỏi trong việc tối ưu hóa mã số.
kirbyfan64sos

Nơi nào bạn tìm thấy một tài liệu tham khảo nói rằng thông số C đảm bảo "int ít nhất là 32 bit"? Các đảm bảo duy nhất tôi biết là cường độ tối thiểu INT_MININT_MAX(-32767 và 32767, trong đó thực tế áp đặt một yêu cầu inttối thiểu là 16 bit). longđược yêu cầu phải lớn nhất bằng một intvà yêu cầu phạm vi có nghĩa longlà ít nhất 32 bit.
ShadowRanger


8

Nhìn vào việc thực hiện Erlang của bạn. Thời gian đã bao gồm khởi động toàn bộ máy ảo, chạy chương trình của bạn và tạm dừng máy ảo. Tôi khá chắc chắn rằng việc thiết lập và tạm dừng erlang vm mất một thời gian.

Nếu thời gian được thực hiện trong chính máy ảo erlang, kết quả sẽ khác vì trong trường hợp đó, chúng tôi sẽ có thời gian thực tế chỉ dành cho chương trình được đề cập. Mặt khác, tôi tin rằng tổng thời gian của quá trình bắt đầu và tải Erlang Vm cộng với việc tạm dừng nó (khi bạn đặt nó trong chương trình của mình) đều được bao gồm trong tổng thời gian mà phương thức bạn đang sử dụng để tính thời gian chương trình đang xuất. Cân nhắc sử dụng chính thời gian erlang mà chúng ta sử dụng khi chúng ta muốn định thời gian cho các chương trình của mình trong chính máy ảo timer:tc/1 or timer:tc/2 or timer:tc/3. Bằng cách này, các kết quả từ erlang sẽ loại trừ thời gian bắt đầu và dừng / tắt / tạm dừng máy ảo. Đó là lý do của tôi ở đó, suy nghĩ về nó, và sau đó thử lại điểm chuẩn của bạn.

Tôi thực sự khuyên chúng ta nên cố gắng định thời gian cho chương trình (đối với các ngôn ngữ có thời gian chạy), trong thời gian chạy của các ngôn ngữ đó để có được giá trị chính xác. Ví dụ, C không có chi phí khởi động và tắt hệ thống thời gian chạy như Erlang, Python và Haskell (chắc chắn 98% về điều này - tôi điều chỉnh đứng). Vì vậy (dựa trên lý do này) tôi kết luận bằng cách nói rằng điểm chuẩn này không đủ chính xác / công bằng cho các ngôn ngữ chạy trên hệ thống thời gian chạy. Hãy làm điều đó một lần nữa với những thay đổi này.

EDIT: bên cạnh ngay cả khi tất cả các ngôn ngữ có hệ thống thời gian chạy, thì chi phí bắt đầu và tạm dừng nó sẽ khác nhau. vì vậy tôi đề nghị chúng ta dành thời gian từ bên trong các hệ thống thời gian chạy (đối với các ngôn ngữ áp dụng điều này). Erlang VM được biết là có chi phí hoạt động đáng kể khi khởi động!


Tôi đã quên đề cập đến nó trong bài viết của mình, nhưng tôi đã đo thời gian cần thiết để khởi động hệ thống (erl -noshell -s erlang tạm dừng) - khoảng 0,1 giây trên máy của tôi. Điều này đủ nhỏ so với thời gian chạy của chương trình (khoảng 10 giây) mà nó không đáng để ngụy biện.
RichardC

trên máy của bạn! chúng tôi không biết liệu bạn có đang làm việc trên máy chủ chống nắng không!. Vì thời gian là một biến số tỷ lệ thuận với thông số kỹ thuật của máy, nên cần xem xét .... phân biệt?
Muzaaya Joshua

2
@RichardC Không nơi nào đề cập rằng Erlang nhanh hơn :) Nó có các mục tiêu khác nhau, không phải tốc độ!
Ngoại lệ

7

Câu hỏi 1: Erlang, Python và Haskell có bị mất tốc độ do sử dụng các số nguyên độ dài tùy ý hay không miễn là các giá trị nhỏ hơn MAXINT?

Câu hỏi một có thể được trả lời trong phủ định cho Erlang. Câu hỏi cuối cùng được trả lời bằng cách sử dụng Erlang một cách thích hợp, như trong:

http://bredsaal.dk/learning-erlang-USE-projecteuler-net

Vì nó nhanh hơn ví dụ C ban đầu của bạn, tôi đoán có rất nhiều vấn đề vì những vấn đề khác đã được đề cập chi tiết.

Mô-đun Erlang này thực thi trên netbook giá rẻ trong khoảng 5 giây ... Nó sử dụng mô hình luồng mạng trong erlang và, như vậy minh họa cách tận dụng mô hình sự kiện. Nó có thể được phân phối trên nhiều nút. Và nó rất nhanh. Không phải mã của tôi.

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

Thử nghiệm dưới đây đã diễn ra trên CPU: Intel (R) Atom (TM) 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

tăng giá trị lên 1000 như dưới đây không thu được kết quả chính xác. Với> 500 như trên, thử nghiệm mới nhất: CPU IntelCore2 6600 @ 2.40GHz trong 0m2.370 thực
Mark Washeim

kết quả của bạn: 76576500 mọi người khác: 842161320 có gì đó không đúng với kết quả của bạn
davidDavidson

Vì tôi là một số vấn đề khác của Euler, tôi chỉ kiểm tra kết quả của mình. Câu trả lời cho projecteuler.net/pro Hiệu = 12 là 76576500 không có câu hỏi nào về nó. Tôi biết nó có vẻ kỳ lạ, nhưng tôi chỉ kiểm tra.
Mark Washeim

Để so sánh, tôi nhận được 9.03 với phiên bản c gốc trong khi sử dụng Erlang 19 với mã của Mark, tôi nhận được 5.406, nhanh hơn 167.0366%.
thanos

5

C ++ 11, <20ms cho tôi - Chạy nó ở đây

Tôi hiểu rằng bạn muốn các mẹo để giúp cải thiện kiến ​​thức cụ thể về ngôn ngữ của mình, nhưng vì nó đã được trình bày rõ ở đây, tôi nghĩ rằng tôi sẽ thêm một số bối cảnh cho những người có thể đã xem xét nhận xét toán học về câu hỏi của bạn, v.v. mã đã chậm hơn rất nhiều.

Câu trả lời này chủ yếu là để cung cấp ngữ cảnh để hy vọng giúp mọi người đánh giá mã trong câu hỏi của bạn / câu trả lời khác dễ dàng hơn.

Mã này chỉ sử dụng một vài tối ưu hóa (xấu xí), không liên quan đến ngôn ngữ được sử dụng, dựa trên:

  1. mọi số traing có dạng n (n + 1) / 2
  2. n và n + 1 là đồng thời
  3. số lượng các ước là một hàm nhân

#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;
}

Mất trung bình khoảng 19ms cho máy tính để bàn của tôi và 80ms cho máy tính xách tay của tôi, khác xa với hầu hết các mã khác tôi đã thấy ở đây. Và không còn nghi ngờ gì nữa, nhiều sự tối ưu vẫn có sẵn.


7
Điều này khá rõ ràng không phải là những gì người hỏi yêu cầu, "Tôi thực sự đã cố gắng thực hiện cùng một thuật toán giống nhau nhất có thể trong bốn ngôn ngữ". Để trích dẫn một nhận xét về một trong nhiều câu trả lời bị xóa tương tự như câu trả lời của bạn "khá rõ ràng bạn có thể có tốc độ nhanh hơn với thuật toán tốt hơn bất kể ngôn ngữ."
Thomas M. DuBuisson

2
@ ThomasM.DuBuisson. Đó là những gì tôi đang nhận được. Câu hỏi \ trả lời rất hàm ý rằng việc tăng tốc thuật toán là rất đáng kể (và tất nhiên OP không yêu cầu họ), nhưng không có ví dụ rõ ràng. Tôi nghĩ rằng câu trả lời này - không phải là mã được tối ưu hóa chính xác - cung cấp một bối cảnh hữu ích nhỏ cho bất kỳ ai, như tôi, người tự hỏi rằng mã của OP chậm / nhanh như thế nào.
dùng3125280

gcc thậm chí có thể tính toán trước rất nhiều mẫu. int a = 0; for (int i = 0; i <10000000; ++ i) {a + = i;} sẽ được tính vào thời gian biên dịch, vì vậy hãy lấy <1ms khi chạy. Nó cũng được tính
Arthur

@Thomas: Tôi phải đồng ý với user3125280 - các ngôn ngữ nên được so sánh với cách chúng làm điều gì đó thông minh thay vì cách chúng không đánh bại một ngôn ngữ lập trình thực sự khi làm điều gì đó ngớ ngẩn. Các thuật toán thông minh thường ít quan tâm đến hiệu quả của kính hiển vi hơn là về tính linh hoạt, khả năng kết nối mọi thứ (kết hợp chúng) và cơ sở hạ tầng. Vấn đề không phải là quá nhiều cho dù người ta nhận được 20 ms hay 50 ms, nó không nhận được 8 giây hay 8 phút.
DarthGizka

5

Đang thử 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
        }
    }
}

Tôi có:

phiên bản c gốc: 9.1690 100%
đi: 8.2520 111%

Nhưng sử dụng:

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
}

Tôi có:

phiên bản c gốc: 9.1690 Phiên bản c thaumkid 100%
: 0.1060 8650%
phiên bản đi đầu tiên: 8.2520 111%
phiên bản thứ hai đi: 0,0230 39865 %

Tôi cũng đã thử Python3.6 và pypy3.3-5.5-alpha:

phiên bản c gốc: 8.629 100%
thaumkid phiên bản c: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

và sau đó với mã sau tôi đã nhận được:

phiên bản c gốc: 8.629 100%
thaumkid phiên bản c: 0.109 8650%
Python3.6: 1.361 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

Thay đổi: case (divisor(T,round(math:sqrt(T))) > 500) of

Đến: case (divisor(T,round(math:sqrt(T))) > 1000) of

Điều này sẽ tạo ra câu trả lời chính xác cho ví dụ đa quy trình Erlang.


2
Đây có phải là một nhận xét về câu trả lời này ? Bởi vì nó không rõ ràng và đây không phải là một câu trả lời.
ShadowRanger

1

Tôi đã đưa ra giả định rằng số lượng các yếu tố chỉ lớn nếu các con số liên quan có nhiều yếu tố nhỏ. Vì vậy, tôi đã sử dụng thuật toán tuyệt vời của thaumkid, nhưng trước tiên sử dụng một xấp xỉ với số lượng nhân tố không bao giờ là quá nhỏ. Điều này khá đơn giản: Kiểm tra các thừa số nguyên tố lên tới 29, sau đó kiểm tra số còn lại và tính giới hạn trên cho bước sóng của các yếu tố. Sử dụng điều này để tính giới hạn trên cho số lượng các yếu tố và nếu số đó đủ cao, hãy tính chính xác số lượng các yếu tố.

Mã dưới đây không cần giả định này cho chính xác, nhưng phải nhanh. Nó dường như làm việc; chỉ khoảng một trong 100.000 số đưa ra ước tính đủ cao để yêu cầu kiểm tra đầy đủ.

Đây là mã:

// 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;
            }
        }
    }
}

Điều này tìm thấy tam giác thứ 14,753,024 với 13824 yếu tố trong khoảng 0,7 giây, số tam giác thứ 879,207,615 với 61,440 yếu tố trong 34 giây, số tam giác 12,524,486,975 với 138,240 nhân tố trong 10 phút 5 giây và 26,467 21 phút 25 giây (Core2 Duo 2,4 GHz), do đó, mã này chỉ mất trung bình 116 chu kỳ bộ xử lý trên mỗi số. Bản thân số tam giác cuối cùng lớn hơn 2 ^ 68, vì vậy


0

Tôi đã sửa đổi phiên bản "Jannich Brendle" thành 1000 thay vì 500. Và liệt kê kết quả của euler12.bin, euler12.erl, p12dist.erl. Cả hai mã erl đều sử dụng '+ gốc' để biên dịch.

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

thời gian ./a.out

2,79 người dùng 0,00s hệ thống 99% tổng cpu 2,79

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.