Порівняння швидкості з Project Euler: C проти Python vs Erlang vs Haskell


672

Я взяв Проблему №12 від Project Euler як вправу програмування та порівняв мої (напевно, не оптимальні) реалізації в C, Python, Erlang та Haskell. Для того, щоб отримати більш високі строки виконання, я шукаю номер першого трикутника з більш ніж 1000 дільниками замість 500, як зазначено в початковій задачі.

Результат такий:

C:

lorenzo@enzo:~/erlang$ gcc -lm -o euler12.bin euler12.c
lorenzo@enzo:~/erlang$ time ./euler12.bin
842161320

real    0m11.074s
user    0m11.070s
sys 0m0.000s

Пітон:

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

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

Python з PyPy:

lorenzo@enzo:~/Downloads/pypy-c-jit-43780-b590cf6de419-linux64/bin$ time ./pypy /home/lorenzo/erlang/euler12.py 
842161320

real    0m13.082s
user    0m13.050s
sys 0m0.020s

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

Підсумок:

  • C: 100%
  • Python: 692% (118% з PyPy)
  • Ерланг: 436% (135% завдяки RichardC)
  • Haskell: 1421%

Я припускаю, що C має велику перевагу, оскільки використовує довгі для обчислень, а не довільні цілі цілі довжини, як інші три. Також не потрібно спочатку завантажувати час виконання (Роблять інші?).

Запитання 1: Чи втрачають Ерланг, Пітон і Хаскелл швидкість через використання довільних цілих чисел довжини чи не вони до тих пір, поки значення меншеMAXINT ?

Питання 2: Чому Haskell так повільно? Чи є прапор компілятора, який вимикає гальмо чи це моя реалізація? (Останнє цілком вірогідне, оскільки Хаскелл - це книга із семи печатками.)

Питання 3: Чи можете ви запропонувати мені деякі підказки, як оптимізувати ці реалізації, не змінюючи спосіб визначення факторів? Оптимізація будь-яким способом: приємніше, швидше, «рідніша» мова.

Редагувати:

Питання 4: Чи дозволяють мої функціональні реалізації LCO (оптимізація останнього дзвінка, також усунення хвостової рекурсії) і, отже, уникати додавання зайвих кадрів у стек викликів?

Я дійсно намагався реалізувати той самий алгоритм максимально схожим на чотирьох мовах, хоча мушу визнати, що мої знання про Haskell та Erlang дуже обмежені.


Використовувані вихідні коди:

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

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

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

#! /usr/bin/env python3.2

import math

def factorCount (n):
    square = math.sqrt (n)
    isquare = int (square)
    count = -1 if isquare == square else 0
    for candidate in range (1, isquare + 1):
        if not n % candidate: count += 2
    return count

triangle = 1
index = 1
while factorCount (triangle) < 1001:
    index += 1
    triangle += index

print (triangle)

-module (euler12).
-compile (export_all).

factorCount (Number) -> factorCount (Number, math:sqrt (Number), 1, 0).

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

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

factorCount (Number, Sqrt, Candidate, Count) ->
    case Number rem Candidate of
        0 -> factorCount (Number, Sqrt, Candidate + 1, Count + 2);
        _ -> factorCount (Number, Sqrt, Candidate + 1, Count)
    end.

nextTriangle (Index, Triangle) ->
    Count = factorCount (Triangle),
    if
        Count > 1000 -> Triangle;
        true -> nextTriangle (Index + 1, Triangle + Index + 1)  
    end.

solve () ->
    io:format ("~p~n", [nextTriangle (1, 1) ] ),
    halt (0).

factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
    where square = sqrt $ fromIntegral number
          isquare = floor square

factorCount' number sqrt candidate count
    | fromIntegral candidate > sqrt = count
    | number `mod` candidate == 0 = factorCount' number sqrt (candidate + 1) (count + 2)
    | otherwise = factorCount' number sqrt (candidate + 1) count

nextTriangle index triangle
    | factorCount triangle > 1000 = triangle
    | otherwise = nextTriangle (index + 1) (triangle + index + 1)

main = print $ nextTriangle 1 1

55
@Jochen (і Сет) Не дуже, що C швидкий або приголомшливий, але це сприймається як легко написати виконавчий код (це може бути неправдою, але більшість програм, здається, здатні, настільки правдиві). Як я досліджую у своїй відповіді, і з часом я виявив, що це правда, важливе значення має майстерність програміста та знання загальних оптимізацій для обраної мови (особливо це стосується Haskell).
Thomas M. DuBuisson

52
Тільки що перевірив з Mathematica - це займає 0.25sec (з C вона приймає 6sec тут), а код просто: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. ура!
цвікас

35
Чи є ще хтось там, хто пам’ятає ці війни між С та зборами? "Звичайно! Ви можете записати свій код на 10 разів швидше на C, але чи може ваш код C запустити це швидко? ..." Я впевнений, що між машинним кодом та складанням велись ті самі битви.
JS.

39
@JS: Мабуть, ні, оскільки збірка - це просто набір мнемоніки, який ви вводите замість необробленого двійкового машинного коду - зазвичай між ними відповідає 1-1.
Callum Rogers

9
висновок для Haskell: -O2 дає йому приблизно 3-кратну швидкість, а використання Int замість Integer приблизно 4х-6x для загальної швидкості 12x-14x і більше.
Буде Несс

Відповіді:


794

Використовуючи GHC 7.0.3, gcc 4.4.6, Linux 2.6.29на x86_64 Core2 Duo (2,5 ГГц) машини, компіляції , використовуючи ghc -O2 -fllvm -fforce-recompдля Haskell і gcc -O3 -lmдля C.

  • Ваш звичайний C працює за 8,4 секунди (швидше, ніж ваш, можливо, через -O3)
  • Рішення Haskell працює за 36 секунд (через -O2прапор)
  • Ваш factorCount'код не набрав явного і неприпустимого Integer(дякую Даніелю за виправлення моєї помилкової діагностики тут!). Надання явного підпису типу (що все одно є стандартною практикою) за допомогою 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 . Без того-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

EDIT: Отже, коли ми це вивчили, давайте вирішувати питання

Запитання 1: Чи втрачають erlang, python та haskell швидкість через використання довільних цілих чисел довжини чи не вони до тих пір, поки значення не перевищують MAXINT?

У Haskell використання Integerповільніше, Intале наскільки повільніше залежить від проведених обчислень. На щастя (для 64-бітних машин) Intдостатньо. Для портативності вам, ймовірно, слід переписати мій код для використання Int64або Word64(C - не єдина мова з a long).

Питання 2: Чому haskell так повільно? Чи є прапор компілятора, який вимикає гальмо чи це моя реалізація? (Останнє цілком вірогідне, оскільки haskell - це книга із семи печатками.)

Питання 3: Чи можете ви запропонувати мені деякі підказки, як оптимізувати ці реалізації, не змінюючи спосіб визначення факторів? Оптимізація будь-яким способом: приємніше, швидше, «рідніша» мова.

На це я відповів вище. Відповідь була

  • 0) Використовуйте оптимізацію через -O2
  • 1) Використовуйте швидкі типи (особливо: розблокувати), коли це можливо
  • 2) remне mod(часто забута оптимізація) і
  • 3) перетворення працівника / обгортки (можливо, найпоширеніша оптимізація).

Питання 4: Чи дозволяють мої функціональні реалізації LCO і, отже, уникати додавання непотрібних кадрів у стек викликів?

Так, це не було проблемою. Гарна робота і рада, що ви це врахували.


25
@Karl Оскільки remце насправді підкомпонент modоперації (вони не однакові). Якщо ви заглянете в бібліотеку бази GHC, ви побачите modтести на кілька умов і відповідно підкоригуєте знак. (див. modInt#у Base.lhs)
Thomas M. DuBuisson

20
Ще один момент даних: я написав а швидкий переклад програми Haskell програми C, не дивлячись на Haskell @ Hyperboreus. Так що це трохи ближче до стандартної идиоматической Haskell, а тільки оптимізація я додав свідомо міняє modз remпісля прочитання цієї відповіді (Хех, ой). Дивіться посилання на мої таймінги, але коротка версія "майже ідентична C".
CA McCann

106
Навіть думав, що версія C працює швидше на моїй машині, я вже поважаю Haskell. +1
Сет Карнегі

11
Це мене дуже дивно, хоча мені ще належить спробувати. Оскільки оригінал factorCount'був хвостовим рекурсивним, я б подумав, що компілятор може помітити, що додаткові параметри не змінюються, і оптимізувати хвостову рекурсію лише для змін параметрів (Haskell є чистою мовою, адже це повинно бути легко). Хтось думає, що компілятор міг би це зробити чи мені слід повернутися, щоб прочитати більше теоретичних робіт?
kizzx2

22
@ kizzx2: Додано квиток GHC . З того, що я зрозумів, це перетворення може призвести до додаткових розподілів об'єктів закриття. Це означає зниження продуктивності в деяких випадках, але, як пропонує Йоган Тібел у своєму дописі на блозі, цього можна уникнути, якщо отриману обгортку можна накреслити.
hammar

224

Є деякі проблеми з реалізацією Erlang. Як базове значення для наступного, мій вимірюваний час виконання вашої немодифікованої програми Erlang становив 47,6 секунди, порівняно з 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;

Код С цього не робить. Взагалі може бути складним порівняння між різними реалізаціями одного і того ж коду, зокрема, якщо алгоритм чисельний, тому що вам потрібно бути впевненим, що вони насправді роблять те саме. Невелика помилка округлення в одній реалізації через деякий набір тексту може призвести до того, що вона зробить набагато більше ітерацій, ніж інша, хоча обидва в підсумку досягають однакового результату.

Щоб усунути це можливе джерело помилок (і позбутися додаткового тесту в кожній ітерації), я переписав функцію factorCount наступним чином, тісно моделюючи код C:

factorCount (N) ->
    Sqrt = math:sqrt (N),
    ISqrt = trunc(Sqrt),
    if ISqrt == Sqrt -> factorCount (N, ISqrt, 1, -1);
       true          -> factorCount (N, ISqrt, 1, 0)
    end.

factorCount (_N, ISqrt, Candidate, Count) when Candidate > ISqrt -> Count;
factorCount ( N, ISqrt, Candidate, Count) ->
    case N rem Candidate of
        0 -> factorCount (N, ISqrt, Candidate + 1, Count + 2);
        _ -> factorCount (N, ISqrt, Candidate + 1, Count)
    end.

Це переписування, ні export_all , і нативна компіляція дала мені наступний час виконання:

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

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

що не дуже погано порівняно з кодом С:

$ time ./a.out 
842161320

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

враховуючи, що Ерланг зовсім не орієнтований на написання числового коду, тому що на 50% повільніше, ніж C, у програмі, як це, дуже добре.

Нарешті, стосовно ваших питань:

Запитання 1: Чи втрачають швидкість erlang, python та haskell через використання цілих чисел довжини чи не вони до тих пір, поки значення не перевищують MAXINT?

Так, дещо. У Ерланге немає способу сказати "використовувати 32/64-бітну арифметику з обертанням", тому, якщо компілятор не зможе довести деякі межі ваших цілих чисел (а зазвичай це не може), він повинен перевірити всі обчислення, щоб побачити якщо вони можуть вміститися в одному слові з тегом, або якщо він повинен перетворити їх у купи, виділені з купи. Навіть якщо жодні бінтуми ніколи не використовуються на практиці під час виконання, ці перевірки доведеться проводити. З іншого боку, це означає, що ви знаєте, що алгоритм ніколи не вийде з ладу через несподіване ціле число, якщо ви раптом дасте йому більші введення, ніж раніше.

Питання 4: Чи дозволяють мої функціональні реалізації LCO і, отже, уникати додавання зайвих кадрів у стек викликів?

Так, ваш код Erlang правильний щодо оптимізації останнього дзвінка.


2
Я погоджуюсь з тобою. Цей орієнтир не був точним особливо для Ерланга з кількох причин
Музая Джошуа

156

Що стосується оптимізації Python, крім використання PyPy (для досить вражаючих прискорень з нульовою зміною коду), ви можете використовувати ланцюжок інструментів перекладу PyPy для складання версії, сумісної з RPython, або Cython для створення модуля розширення, обидва які швидше, ніж версія C на моєму тестуванні, з модулем Cython майже вдвічі швидше . Для довідки я включаю також результати порівняння C та PyPy:

C (складено з gcc -O3 -lm)

% time ./euler12-c 
842161320

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

PyPy 1.5

% time pypy euler12.py
842161320
pypy euler12.py  
16.44s user 
0.01s system 
99% cpu 16.449 total

RPython (використовуючи останню версію PyPy, c2f583445aee)

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

Цитон 0,15

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

У версії RPython є кілька ключових змін. Для перекладу в окрему програму вам потрібно визначити свою target, яка в даному випадку єmain функцією. Очікується, що він прийме sys.argvяк лише аргумент, і він повинен повернути int. Ви можете перекласти його, скориставшись translate.py, % translate.py euler12-rpython.pyякий перекладається на C і компілює його для вас.

# euler12-rpython.py

import math, sys

def factorCount(n):
    square = math.sqrt(n)
    isquare = int(square)
    count = -1 if isquare == square else 0
    for candidate in xrange(1, isquare + 1):
        if not n % candidate: count += 2
    return count

def main(argv):
    triangle = 1
    index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle
    return 0

if __name__ == '__main__':
    main(sys.argv)

def target(*args):
    return main, None

Версія Cython була переписана як модуль розширення _euler12.pyx, який я імпортую та закликаю із звичайного файлу python. The_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, написання ваших CPU-інтенсивних бітів коду в модуль розширення Cython здається дійсно простим способом оптимізації вашої програми.


6
Мені цікаво, чи можна оптимізувати версію С принаймні так швидко, як CPython?
Відображати ім'я

4
@SargeBorsch, що версія Cython настільки швидка, тому що вона збирається до високооптимізованого джерела C, а це означає, що ви впевнені, що зможете отримати таку ефективність із C.
Елі Корвіго

72

Питання 3: Чи можете ви запропонувати мені деякі підказки, як оптимізувати ці реалізації, не змінюючи спосіб визначення факторів? Оптимізація будь-яким способом: приємніше, швидше, «рідніша» мова.

Реалізація C є неоптимальною (як натякав Томас М. Дюбайсон), версія використовує 64-бітні цілі числа (тобто довгі тип даних). Я буду досліджувати список складання пізніше, але, з освіченою здогадкою, у складеному коді відбувається деякий доступ до пам’яті, завдяки чому використання 64-бітових цілих чисел значно повільніше. Це той чи згенерований код (будь то факт, що ви можете помістити менше 64-бітових входів у реєстр SSE або округлити подвійне до 64-бітного цілого числа повільніше).

Ось модифікований код (просто замініть long на int, і я явно інлігував factorCount, хоча я не думаю, що це потрібно з gcc -O3):

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

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

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

Запуск + терміни, які він дає:

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

Для довідки, реалізація haskell Томасом у попередній відповіді дає:

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

Висновок: не відриваючи нічого від ghc, його чудовий компілятор, але gcc зазвичай генерує швидший код.


22
Дуже хороший! Для порівняння, на моїй машині ваше C-рішення працює, 2.5 secondsтоді як аналогічна модифікація коду Haskell (перехід до Word32, додавання прагми INLINE) призводить до виконання часу 4.8 seconds. Можливо, щось можна зробити (не потрійно, здається) - результат gcc, безумовно, вражаючий.
Thomas M. DuBuisson

1
Дякую! Можливо, питання має полягати в швидкості складеного виводу різними компіляторами, а не власне мовою. Потім знову витягнення посібників Intel і оптимізація вручну все одно виграють прямо (за умови, що у вас є знання та час (багато)).
Raedwulf

56

Погляньте на цей блог . За останній рік або близько того він вирішив кілька проблем Project Euler в Haskell і Python, і він, як правило, вважає Haskell набагато швидшим. Я думаю, що між цими мовами це має більше спільного з вашим вільним володінням та стилем кодування.

Якщо мова йде про швидкість Python, ви використовуєте неправильну реалізацію! Спробуйте PyPy , і для таких речей ви побачите, що це набагато, набагато швидше.


32

Ваша реалізація Haskell може бути значно прискорена, використовуючи деякі функції з пакетів Haskell. У цьому випадку я використовував праймери, які просто встановлені з 'cabal install primes';)

import Data.Numbers.Primes
import Data.List

triangleNumbers = scanl1 (+) [1..]
nDivisors n = product $ map ((+1) . length) (group (primeFactors n))
answer = head $ filter ((> 500) . nDivisors) triangleNumbers

main :: IO ()
main = putStrLn $ "First triangle number to have over 500 divisors: " ++ (show answer)

Терміни:

Ваша оригінальна програма:

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

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Поліпшена реалізація

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

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Як бачите, ця працює за 38 мілісекунд на тій же машині, де ваша бігла за 16 секунд :)

Команди компіляції:

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

5
Востаннє я перевірив, що Haskell "праймес" був просто величезним списком попередньо обчислених прайменів - ніяких обчислень, просто пошуку. Так, так, звичайно, це буде швидше, але це нічого не говорить про обчислювальну швидкість отримання праймерів у Haskell.
zxq9

21
@ zxq9 Ви могли б мені вказати, де в джерелі пакету primes ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ) знаходиться список простих чисел?
Фрейзер

4
Хоча джерело показує, що прайми не розраховані, ця швидкість є абсолютно божевільною, милі швидше, ніж версія C, так що, до біса, відбувається?
крапка з комою

1
@semicolon запам'ятовування. У цьому випадку я думаю, що Haskell запам'ятовував усі праймери під час виконання, тому не потрібно перераховувати їх кожну ітерацію.
Hauleth

5
Це 1000 дільників, а не 500.
Casper 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 це, послідовно працює на моїй машині 0,55-0,58 секунди (1,73GHz Core i7).

Більш ефективна функція factorCount для версії 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;
}

Зміна довготи на основну, використовуючи gcc -O3 -lmце, послідовно працює за 0,31-0,35 секунди.

І те й інше можна змусити запустити ще швидше, якщо скористатися тим фактом, що число n-го трикутника = n * (n + 1) / 2, і n і (n + 1) мають цілком розрізнені прості множники, тому кількість факторів кожної половини можна множити, щоб знайти кількість факторів у цілому. Наступні:

int main ()
{
  int triangle = 0,count1,count2 = 1;
  do {
    count1 = count2;
    count2 = ++triangle % 2 == 0 ? factorCount(triangle+1) : factorCount((triangle+1)/2);
  } while (count1*count2 < 1001);
  printf ("%lld\n", ((long long)triangle)*(triangle+1)/2);
}

скоротить час запуску коду до 0,17-0,19 секунди, і він може обробляти набагато більші пошуки - більше ніж 10000 факторів займає близько 43 секунд на моїй машині. Я залишаю аналогічну швидкість haskell зацікавленому читачеві.


3
Просто для порівняння: оригінальна версія версії c: 9.1690, версія thaumkid: 0.1060 поліпшення 86x.
thanos

Ого. Haskell чудово виконує, коли ви уникаєте
підведених

Насправді це не висновок, який це робив. Це лише допомагає вам A) налагоджувати або уникати проблем типу та проблем з вибором екземплярів typeclass B) налагодження та уникнути деяких невідрізних проблем типу з кількома сучасними розширеннями мови. Це також допомагає зробити ваші програми непридатними, так що ви ніколи не зможете розширити свої зусилля з розвитку.
codehot

c версія 0.11 s на каньйоні черепа Intel
кодекс

13
Запитання 1: Чи втрачають швидкість erlang, python та haskell через використання цілих чисел довжини чи не вони до тих пір, поки значення не перевищують MAXINT?

Це малоймовірно. Я не можу сказати багато про Ерланг і Хаскелл (ну, можливо, трохи про Хаскелл нижче), але я можу вказати на багато інших вузьких місць в Python. Кожен раз, коли програма намагається виконати операцію з деякими значеннями в Python, вона повинна перевірити, чи є значення належного типу, і це коштує трохи часу. Ваша factorCountфункція просто виділяє список із range (1, isquare + 1)різним часом, а mallocрозподіл пам’яті в стилі виконання за часом виконання проходить повільніше, ніж повторення в діапазоні з лічильником, як це робиться в C. Помітно, factorCount()це називається кілька разів, і таким чином виділяється багато списків. Крім того, не будемо забувати, що Python інтерпретується, а інтерпретатор CPython не має великої уваги до оптимізації.

EDIT : о, добре, зауважу, що ви використовуєте Python 3, тому range()повертає не список, а генератор. У цьому випадку моя думка щодо розподілу списків є напівпомилковою: функція просто виділяє rangeоб'єкти, які все ж є неефективними, але не настільки ефективними, як розподіл списку з великою кількістю елементів.

Питання 2: Чому haskell так повільно? Чи є прапор компілятора, який вимикає гальмо чи це моя реалізація? (Останнє цілком вірогідне, оскільки haskell - це книга із семи печатками.)

Ви використовуєте обійми ? Hugs - це значно повільний перекладач. Якщо ви користуєтесь цим, можливо, ви зможете краще провести час з GHC - але я лише гігітезую, що таке хороший компілятор Haskell під кришкою є досить захоплюючим і набагато перевищує моє розуміння :)

Питання 3: Чи можете ви запропонувати мені деякі підказки, як оптимізувати ці реалізації, не змінюючи спосіб визначення факторів? Оптимізація будь-яким способом: приємніше, швидше, «рідніша» мова.

Я б сказав, що ви граєте в недобру гру. Найкраща частина знань різних мов - це використовувати їх самим різним способом :) Але я відступаю, просто не маю жодної рекомендації щодо цього. Вибачте, сподіваюся, хтось може допомогти вам у цьому випадку :)

Питання 4: Чи дозволяють мої функціональні реалізації LCO і, отже, уникати додавання зайвих кадрів у стек викликів?

Наскільки я пам’ятаю, вам просто потрібно переконатися, що ваш рекурсивний виклик є останньою командою перед поверненням значення. Іншими словами, така функція, як наведена нижче, може використовувати таку оптимізацію:

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

Однак у вас не було б такої оптимізації, якби ваша функція була такою, як наведена нижче, оскільки після рекурсивного виклику відбувається операція (множення):

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

Я розділив операції в деяких локальних змінних, щоб зрозуміти, які операції виконуються. Однак найбільш звичайним є розгляд цих функцій, як показано нижче, але вони еквівалентні моменту, яку я створюю:

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

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

Зауважте, що компілятор / інтерпретатор вирішуватиме, чи зробить це хвостову рекурсію. Наприклад, інтерпретатор Python не робить цього, якщо я добре пам’ятаю (я використовував Python у своєму прикладі лише через його синтаксис, що вільно говорить). У будь-якому випадку, якщо ви знайдете дивні речі, такі як факториальні функції з двома параметрами (і один з параметрів має такі назви, як accі accumulatorт.д.), тепер ви знаєте, чому це роблять люди :)


@Hyperboreus дякую! Також мені дуже цікаво про ваші наступні запитання. Однак я попереджаю вас про те, що мої знання обмежені, тому я не міг відповісти на кожне ваше питання. За спробу компенсувати це я зробив свою вікі спільноти відповідей, щоб люди могли легше доповнювати її.
brandizzi

Про використання дальності. Коли я замінюю діапазон циклом на час з збільшенням (імітуючи цикл for C), час виконання фактично подвоюється. Я думаю, генератори досить оптимізовані.
Гіперборей

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 ) на моїй машині з GHC 7.6.2, тоді як версія C від @Raedwulf працює ~ 3.15s . Здається, GHC покращився за рік.

PS. Я знаю, що це старе питання, і я натрапляю на нього з пошуку Google (я забув, що шукав, зараз ...). Просто хотів прокоментувати питання про LCO та висловити свої почуття щодо Haskell взагалі. Я хотів прокоментувати верхню відповідь, але коментарі не дозволяють блокувати коди.


9

Ще кілька номерів та пояснень для версії C. Мабуть, ніхто не робив це протягом усіх тих років. Не забудьте підкріпити цю відповідь, щоб вона змогла побачити і дізнатись усіх.

Крок перший: Орієнтир авторських програм

Технічні характеристики ноутбука:

  • Процесор i3 M380 (931 МГц - максимальний режим економії акумулятора)
  • 4 Гб пам'яті
  • Win7 64 біт
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin з gcc 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

  • integertype такий же, як і оригінальна програма на даний момент (докладніше про це пізніше)
  • архітектура становить x86 або x64, залежно від параметрів компілятора
  • компілятор - gcc або vs2012

Крок другий: досліджуйте, вдосконалюйте та оцінюйте ще раз

VS на 250% швидше, ніж gcc. Два компілятори повинні давати однакову швидкість. Очевидно, що з кодом чи параметрами компілятора щось не так. Давайте дослідимо!

Перша цікава точка - цілі типи. Перетворення можуть бути дорогими, а послідовність важлива для кращого генерування коду та оптимізації. Усі цілі числа повинні бути одного типу.

Це змішаний безлад intі longзараз. Ми будемо вдосконалювати це. Який тип використовувати? Найшвидший. Ви повинні їх орієнтир все!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Цілі типи є int long int32_t uint32_t int64_tі uint64_tвід#include <stdint.h>

У С є багато цілих типів, плюс кілька підписаних / непідписаних для гри, плюс вибір для компіляції як x86 або x64 (не плутати з фактичним цілим розміром). Це багато версій для компіляції та запуску ^^

Крок третій: Розуміння чисел

Остаточні висновки:

  • Цілі числа 32 біт ~ на 200% швидше, ніж 64 бітові еквіваленти
  • цілі числа без підпису 64-бітові на 25% швидші, ніж підписані 64 біти (на жаль, я не маю пояснення тому)

Хитромудрий питання: "Які розміри int і long у С?"
Правильна відповідь: розмір int і long в C не є чітко визначеними!

З специфікації C:

int - принаймні 32 біта
, щонайменше - int

На сторінці gcc man (прапорці -m32 та -m64):

32-бітове середовище встановлює int, long і покажчик на 32 біта і генерує код, який працює в будь-якій системі i386.
64-бітове середовище встановлює int до 32 біт і довге, а покажчик - на 64 біт і генерує код для архітектури x86-64 AMD.

З документації MSDN (діапазони типів даних) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 байти, також знає, як
довго підписаний , 4 байти, також знає, як довгий int і підписаний long int

Щоб зробити висновок про це: засвоєні уроки

  • 32 біт цілих чисел швидше, ніж 64 біт цілих чисел.

  • Стандартні типи цілих чисел недостатньо чітко визначені в C і C ++, вони змінюються залежно від компіляторів та архітектури. Коли вам потрібна послідовність і передбачуваність, використовуйте uint32_tцілу сім'ю від #include <stdint.h>.

  • Вирішені проблеми швидкості. Усі інші мови відстають на сто відсотків, C & C ++ знову виграють! Вони завжди так роблять. Наступним поліпшенням буде багатопотокове використання за допомогою OpenMP: D


Як цікаво робити компілятори Intel? Зазвичай вони дуже хороші в оптимізації числового коду.
kirbyfan64sos

Де ви знайдете посилання, яке говорить про те, що специфікація C гарантує, що "int принаймні 32 біта"? Єдині мені відомі гарантії - це мінімальні величини INT_MINта INT_MAX(-32767 та 32767, які практично пред'являють вимогу, яка intстановить принаймні 16 біт). longПотрібно бути принаймні таким же величиною, як і int, а середні вимоги до діапазону long- не менше 32 біт.
ShadowRanger


8

Дивлячись на вашу реалізацію Erlang. Час включав запуск всієї віртуальної машини, запуск вашої програми та зупинку віртуальної машини. Я впевнений, що налаштування та зупинка програми erlang vm потребує певного часу.

Якби терміни проводилися у самій віртуальній машині erlang, результати були б іншими, оскільки в такому випадку у нас був би фактичний час лише для відповідної програми. В іншому випадку я вважаю, що загальний час, зайнятий процесом запуску та завантаження Erlang Vm, плюс час його зупинки (як ви це помітили у програмі), всі вони включаються до загального часу, який ви використовуєте методом, щоб затримати час програма виводить. Подумайте про те, щоб використовувати сам термін erlang, який ми використовуємо, коли ми хочемо проводити час нашої програми у самій віртуальній машині timer:tc/1 or timer:tc/2 or timer:tc/3. Таким чином, результати від erlang виключать час, необхідний для запуску та зупинки / вбиття / зупинки віртуальної машини. Це моє міркування там, подумайте над цим, а потім спробуйте ще раз.

Я насправді пропоную спробувати вчасно програму (для мов, які мають час виконання), протягом часу виконання цих мов, щоб отримати точне значення. Наприклад, C не має накладних витрат на запуск та вимикання системи виконання, як у Erlang, Python та Haskell (98% впевнених у цьому - я виправлю корекцію). Отже (виходячи з цього міркування) я закінчую, кажучи, що цей показник був недостатньо точним / справедливим для мов, що працюють на вершині системи виконання. Давайте зробимо це знову з цими змінами.

EDIT: окрім навіть, якщо б усі мови мали системи виконання, витрати на запуск кожної та зупинки будуть різними. тож я пропоную нам час із систем виконання (для мов, для яких це стосується). Відомо, що Erlang VM має значні накладні витрати при запуску!


Я забув згадати це у своєму дописі, але я виміряв час, необхідний лише для запуску системи (erl -noshell -s erlang halt) - близько 0,1 секунди на моїй машині. Це досить мало в порівнянні з часом виконання програми (близько 10 секунд), що не варто кайфувати.
RichardC

на вашій машині! ми не знаємо, чи працюєте ви на сервері сонячного вогню !. Оскільки час є змінним, пропорційним технічним характеристикам, його слід враховувати .... хихикати?
Музая Джошуа

2
@RichardC Ніде не було сказано, що Ерланг швидший :) У нього різні цілі, а не швидкість!
Виняток

7

Запитання 1: Чи втрачають Ерланг, Пітон та Хаскелл швидкість через використання довільних цілих чисел довжини чи не вони до тих пір, поки значення менші MAXINT?

На одне питання можна відповісти негативно для Ерланга. На останнє запитання відповідають, використовуючи 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) N270 при 1,60 ГГц

~$ 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 6600 @ 2,40 ГГц поєднується в реальному 0м2.370-х
Марк Уасхайм

ваш результат: 76576500 всі інші: 842161320 тер є щось, що відповідає вашим результатам
davidDavidson

Оскільки у мене були деякі інші проблеми Ейлера, я просто перевірив свій результат. Відповідь на projecteuler.net/problem=12 це 76576500 не питання про це. Я знаю, що це здається дивним, але я просто перевірив.
Марк Васхайм

Для порівняння я отримую 9.03 з оригінальною версією c, використовуючи Erlang 19 з кодом Марка, я набираю 5.406, 167.0366% швидше.
thanos

5

C ++ 11, <20 мс для мене - запустіть його тут

Я розумію, що ви хочете поради, щоб допомогти вдосконалити свої знання з мовної мови, але, оскільки це добре висвітлено тут, я подумав, що я додам контекст для людей, які, можливо, подивилися на коментар математики до вашого питання тощо, і запитав, чому це код був набагато повільніше.

Ця відповідь головним чином полягає в наданні контексту, щоб, сподіваючись, допомогти людям легше оцінити код у вашому запитанні / інші відповіді.

Цей код використовує лише пару (потворних) оптимізацій, не пов'язаних із використовуваною мовою, виходячи з:

  1. кожне число трійки має вигляд n (n + 1) / 2
  2. n і n + 1 є спірними
  3. кількість дільників - це мультиплікативна функція

#include <iostream>
#include <cmath>
#include <tuple>
#include <chrono>

using namespace std;

// Calculates the divisors of an integer by determining its prime factorisation.

int get_divisors(long long n)
{
    int divisors_count = 1;

    for(long long i = 2;
        i <= sqrt(n);
        /* empty */)
    {
        int divisions = 0;
        while(n % i == 0)
        {
            n /= i;
            divisions++;
        }

        divisors_count *= (divisions + 1);

        //here, we try to iterate more efficiently by skipping
        //obvious non-primes like 4, 6, etc
        if(i == 2)
            i++;
        else
            i += 2;
    }

    if(n != 1) //n is a prime
        return divisors_count * 2;
    else
        return divisors_count;
}

long long euler12()
{
    //n and n + 1
    long long n, n_p_1;

    n = 1; n_p_1 = 2;

    // divisors_x will store either the divisors of x or x/2
    // (the later iff x is divisible by two)
    long long divisors_n = 1;
    long long divisors_n_p_1 = 2;

    for(;;)
    {
        /* This loop has been unwound, so two iterations are completed at a time
         * n and n + 1 have no prime factors in common and therefore we can
         * calculate their divisors separately
         */

        long long total_divisors;                 //the divisors of the triangle number
                                                  // n(n+1)/2

        //the first (unwound) iteration

        divisors_n_p_1 = get_divisors(n_p_1 / 2); //here n+1 is even and we

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1);   //n_p_1 is now odd!

        //now the second (unwound) iteration

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1 / 2);   //n_p_1 is now even!
    }

    return (n * n_p_1) / 2;
}

int main()
{
    for(int i = 0; i < 1000; i++)
    {
        using namespace std::chrono;
        auto start = high_resolution_clock::now();
        auto result = euler12();
        auto end = high_resolution_clock::now();

        double time_elapsed = duration_cast<milliseconds>(end - start).count();

        cout << result << " " << time_elapsed << '\n';
    }
    return 0;
}

Це займає близько 19 мс в середньому для мого робочого столу і 80 мс для мого ноутбука, далеко від більшості інших кодів, які я бачив тут. І, без сумніву, ще багато оптимізацій.


7
Це явно не те, що запитував запитувач: "Я дійсно намагався реалізувати той самий алгоритм максимально схожим на чотирьох мовах". Процитуючи коментар до однієї з багатьох видалених відповідей, подібних до вашої, "це цілком очевидно, що ви можете отримати більш високі швидкості за допомогою кращого алгоритму незалежно від мови".
Thomas M. DuBuisson

2
@ ThomasM.DuBuisson. Ось в чому я і отримую. З питання \ відповіді багато в чому випливає, що алгоритмічні прискорення швидкості значні (і, звичайно, ОП їх не вимагає), але явного прикладу немає. Я думаю, що ця відповідь - яка не є дуже оптимізованим кодом - дає трохи корисного контексту для тих, хто, як я, хто замислювався про те, наскільки повільний / швидкий був код ОП.
user3125280

gcc може навіть попередньо обчислити безліч шаблонів. int a = 0; для (int i = 0; i <10000000; ++ i) {a + = i;} буде обчислено під час компіляції, тому візьміть <1 мс під час виконання. Це також рахується
Артур

@Thomas: Я повинен погодитися з user3125280 - мови повинні порівнюватися, як вони роблять щось розумне, а не як вони не можуть перемогти справжню мову програмування, роблячи щось німе. Розумні алгоритми зазвичай менше піклуються про мікроскопічну ефективність, ніж про гнучкість, здатність передавати речі (поєднувати їх) та інфраструктуру. Справа не в тому, чи отримуєш 20 мс або 50 мс, не 8 секунд чи 8 хвилин.
DarthGizka

5

Спроба GO:

package main

import "fmt"
import "math"

func main() {
    var n, m, c int
    for i := 1; ; i++ {
        n, m, c = i * (i + 1) / 2, int(math.Sqrt(float64(n))), 0
        for f := 1; f < m; f++ {
            if n % f == 0 { c++ }
    }
    c *= 2
    if m * m == n { c ++ }
    if c > 1001 {
        fmt.Println(n)
        break
        }
    }
}

Я отримав:

оригінальна версія версії: 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: 0.1060 8650%
перша версія: 8.2520 111%
друга версія: 0,0230 39865%

Я також спробував Python3.6 та pypy3.3-5.5-alpha:

оригінальна версія в версії: 8.629 100%
версія таумкіда: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

а потім із наступним кодом я отримав:

оригінальна версія версії: 8.629 100%
версія тамкіда: 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 цифр дає оцінку, достатньо високу, щоб вимагати повної перевірки.

Ось код:

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

Це знаходить 14,753,024 трикутне з 13824 факторами приблизно за 0,7 секунди, 879,207,615 трикутне число з 61,440 коефіцієнтами за 34 секунди, 12,524,486,975-го трикутного числа зі 138,240 коефіцієнтами за 10 хвилин 5 секунд, а 26,467,792,064-го трикутного числа з 17232 коефіцієнтами в 21 хвилина 25 секунд (2,4 ГГц Core2 Duo), тому цей код в середньому займає лише 116 процесорних циклів на число. Саме останнє трикутне число більше 2 ^ 68, так


0

Я змінив версію "Jannich Brendle" на 1000 замість 500. І перерахував результат euler12.bin, euler12.erl, p12dist.erl. Обидва коди erl використовують для компіляції "+ native".

zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s p12dist start
The result is: 842161320.

real    0m3.879s
user    0m14.553s
sys     0m0.314s
zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s euler12 solve
842161320

real    0m10.125s
user    0m10.078s
sys     0m0.046s
zhengs-MacBook-Pro:workspace zhengzhibin$ time ./euler12.bin 
842161320

real    0m5.370s
user    0m5.328s
sys     0m0.004s
zhengs-MacBook-Pro:workspace zhengzhibin$

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 всього

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.