Сравнение скорости с Project Euler: C против Python против Erlang против Haskell

519

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

В результате получается следующее:

С

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

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

Python:

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

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

Python с PyPy:

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

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

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)
  • Erlang: 436% (135% благодаря RichardC)
  • Haskell: 1421%

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

Вопрос 1: Неужели Erlang, Python и Haskell теряют скорость из-за использования произвольных целых чисел или не до тех пор, пока значения меньше MAXINT?

Вопрос 2: Почему Хаскелл так медленно? Есть ли флаг компилятора, который отключает тормоза или это моя реализация? (Последнее вполне вероятно, так как Haskell - это книга с семью печатями для меня.)

Вопрос 3: Можете ли вы предложить мне несколько советов, как оптимизировать эти реализации без изменения способа определения факторов? Оптимизация в любом случае: более приятная, быстрая, более "родная" для языка.

EDIT:

Вопрос 4: Могут ли мои функциональные реализации разрешить LCO (последняя оптимизация вызовов, устранение хвостовой регенерации a.k.a) и, следовательно, избежать добавления ненужных кадров в стек вызовов?

Я действительно пытался реализовать тот же алгоритм, который был бы как можно более похожим на четырех языках, хотя я должен признать, что мои знания 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
  • 1
    Существует веб-страница, связанная исключительно с отображением различий в скорости языков. Основываясь на ваших данных в сравнении со страницей перестрелки, я бы сказал, что ваш алгоритм не так хорош, как некоторые другие, используемые на странице перестрелки.
  • 54
    @Jochen (и Seth) Не совсем то, что C быстрый или потрясающий, но это воспринимается как простой способ написания производительного кода (это может быть не так, но большинство программ, кажется, способны, настолько верно). Как я выяснил в своем ответе и обнаружил, что со временем это так, навыки программиста и знание общих оптимизаций для выбранного языка имеют большое значение (особенно для Haskell).
Показать ещё 23 комментария
Теги:
performance
haskell
erlang

19 ответов

703
Лучший ответ

Используя 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 преуспевает.

Заключение: Haskell потрясающий.

Результирующий код

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

factorCount' :: Int -> Int -> Int -> Int -> Int
factorCount' number sqrt candidate0 count0 = go candidate0 count0
  where
  go candidate count
    | candidate > sqrt = count
    | number `rem` candidate == 0 = go (candidate + 1) (count + 2)
    | otherwise = go (candidate + 1) count

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

main = print $ nextTriangle 1 1

EDIT: Теперь, когда мы это изучили, давайте рассмотрим вопросы

Вопрос 1: ли erlang, python и haskell теряют скорость из-за использования произвольные длины целых чисел или не до тех пор, пока значения меньше чем MAXINT?

В Haskell использование Integer происходит медленнее, чем Int, но насколько медленнее зависит от выполненных вычислений. К счастью (для 64-битных машин) достаточно Int. Для переносимости вы, вероятно, должны переписать мой код для использования Int64 или Word64 (C не является единственным языком с long).

Вопрос 2: Почему haskell так медленно? Есть ли флаг компилятора, который выключает тормоза или это моя реализация? (Последнее вполне вероятно, как haskell - это книга с семью печатями для меня.)

Вопрос 3: Можете ли вы предложить мне несколько советов по оптимизации этих не изменяя способ определения факторов? Оптимизация в любом случае: более приятная, быстрая, более "родная" для языка.

Вот что я ответил выше. Ответ был

  • 0) Используйте оптимизацию с помощью -O2
  • 1) По возможности используйте быстрые (особенно: unbox-able) типы
  • 2) rem not mod (часто забываемая оптимизация) и
  • 3) преобразование рабочего/обертки (возможно, наиболее распространенная оптимизация).

Вопрос 4: Могут ли мои функциональные реализации LCO и, следовательно, избегать добавления ненужных кадров в стек вызовов?

Да, это не проблема. Хорошая работа и рад, что вы это рассмотрели.

  • 3
    Почему между rem и mod такая разница в скорости?
  • 25
    @Karl Потому что rem на самом деле является подкомпонентом операции mod (они не одинаковы). Если вы заглянете в библиотеку GHC Base, вы увидите mod тесты для нескольких условий и соответственно измените знак. (см. modInt# в Base.lhs )
Показать ещё 19 комментариев
213

Есть некоторые проблемы с реализацией Erlang. В качестве базового значения для следующего, мое измеренное время выполнения для вашей неизмененной программы Erlang составляло 47,6 секунды, по сравнению с 12,7 секундами для кода C.

Первое, что вам нужно сделать, если вы хотите запустить вычислительный интенсивный код Erlang, - это использовать собственный код. Компиляция с erlc +native euler12 получила время до 41,3 секунды. Это, однако, гораздо более низкое ускорение (всего 15%), чем ожидалось от встроенной компиляции на этом виде кода, и проблема заключается в использовании -compile(export_all). Это полезно для экспериментов, но тот факт, что все функции потенциально доступны извне, приводит к тому, что собственный компилятор очень консервативен. (Нормальный эмулятор BEAM не так сильно затронут.) Замена этого объявления на -export([solve/0]). дает гораздо лучшее ускорение: 31,5 секунды (почти 35% от базовой линии).

Но сам код имеет проблему: для каждой итерации в цикле factorCount вы выполняете этот тест:

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

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

Чтобы устранить этот возможный источник ошибок (и избавиться от дополнительного теста на каждой итерации), я переписал функцию 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.

Эта перезапись, no export_all и встроенная компиляция дали мне следующее время выполнения:

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

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

что не так уж плохо по сравнению с кодом C:

$ time ./a.out 
842161320

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

учитывая, что Erlang вовсе не предназначен для написания числового кода, но на 50% медленнее, чем C в такой программе, это довольно хорошо.

Наконец, в отношении ваших вопросов:

Вопрос 1: освободите скорость erlang, python и haskell из-за использования произвольных целых чисел или не так ли, если значения меньше MAXINT?

Да, несколько. В Erlang нет способа сказать "использовать 32/64-битную арифметику с оберткой", поэтому, если компилятор не может доказать некоторые ограничения на ваши целые числа (и обычно это невозможно), он должен проверить все вычисления, чтобы увидеть если они могут вписаться в одно помеченное слово или если оно должно превратить их в выделенные кучи бонусы. Даже если никакие бингомы никогда не используются на практике во время выполнения, эти проверки должны быть выполнены. С другой стороны, это означает, что вы знаете, что алгоритм никогда не будет терпеть неудачу из-за неожиданного целого числа, если вы вдруг придадите ему большие входы, чем раньше.

Вопрос 4. Могут ли мои функциональные реализации разрешить LCO и, следовательно, избежать добавления ненужных кадров в стек вызовов?

Да, ваш код Erlang верен в отношении оптимизации последнего вызова.

  • 1
    Я с тобой согласен. Этот тест не был точным, особенно для Эрланга по ряду причин
142

В отношении оптимизации Python, помимо использования PyPy (для довольно впечатляющих ускорений с нулевым изменением кода), вы можете использовать PyPy toolchain для перевода для компиляции версии, совместимой с 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

Cython 0.15

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

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

# euler12-rpython.py

import math, sys

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

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

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

def target(*args):
    return main, None

Версия Cython была переписана как модуль расширения _euler12.pyx, который я импортирую и вызываю из обычного файла python. _euler12.pyx по существу совпадает с вашей версией, с некоторыми дополнительными объявлениями статического типа. У setup.py есть нормальный шаблон для построения расширения, используя python setup.py build_ext --inplace.

# _euler12.pyx
from libc.math cimport sqrt

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

cpdef main():
    cdef int triangle = 1, index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle

# euler12-cython.py
import _euler12
_euler12.main()

# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("_euler12", ["_euler12.pyx"])]

setup(
  name = 'Euler12-Cython',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

У меня, честно говоря, очень мало опыта работы с RPython или Cython, и я был приятно удивлен результатами. Если вы используете CPython, запись ваших процессорных бит кода в модуле расширения Cython кажется очень простым способом оптимизации вашей программы.

  • 6
    Мне любопытно, можно ли оптимизировать версию C так, чтобы она была по крайней мере такой же быстрой, как CPython?
  • 2
    @SargeBorsch, что версия Cython такая быстрая, потому что она компилируется в высокооптимизированный исходный код на C, что означает, что вы наверняка сможете получить эту производительность от C.
66

Вопрос 3: Можете ли вы предложить мне несколько советов, как оптимизировать эти реализации не меняя способ определения факторов? Оптимизация в любом путь: лучше, быстрее, более "родной" для языка.

Реализация C субоптимальна (как намекает Томас М. Дюбуиссон), версия использует 64-битные целые числа (т.е. тип длинный). Далее я расскажу об ассемблере, но с образованной догадкой в ​​скомпилированном коде происходят некоторые обращения к памяти, которые значительно ускоряют использование 64-битных целых чисел. Это то, что или сгенерированный код (будь то тот факт, что вы можете поместить менее 64-битных int в регистр 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 обычно генерирует более быстрый код.

  • 17
    Очень хорошо! Для сравнения, на моем компьютере ваше решение C запускается за 2.5 seconds а аналогичная модификация кода на Haskell (переход на Word32, добавление прагмы INLINE) приводит к времени выполнения 4.8 seconds . Возможно, что-то можно сделать (кажется, не тривиально) - результат gcc, безусловно, впечатляет.
  • 0
    Спасибо! Возможно, вопрос должен заключаться в скорости компиляции различных компиляторов, а не в самом языке. Опять же, извлечение руководств Intel и оптимизация вручную все равно выиграют (если у вас есть знания и время (много)).
51

Взгляните на этот блог. За последний год или около того он сделал несколько проблем Project Euler в Haskell и Python, и он, как правило, обнаружил, что Haskell намного быстрее. Я думаю, что между этими языками он больше связан с вашим беглостью и стилем кодирования.

Когда дело доходит до скорости Python, вы используете неправильную реализацию! Попробуйте PyPy, и для таких вещей вы найдете намного больше, намного быстрее.

  • 24
    Pypy действительно сделал разницу.
28

Ваша реализация Haskell может быть значительно ускорена за счет использования некоторых функций из пакетов Haskell. В этом случае я использовал простые числа, которые только что были установлены с "номерами для установки" 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)

Тайминги:

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

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.
  • 20
    @ zxq9 не могли бы вы указать мне, где в источнике пакета простых чисел ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ) находится список простых чисел?
Показать ещё 6 комментариев
23

Просто для удовольствия. Ниже приведена более "нативная" реализация 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.73 ГГц 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;
}

Изменение longs для ints в main, используя 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);
}

сократит время выполнения кода c до 0,17-0,19 секунды, и оно сможет обрабатывать гораздо более крупные поисковые запросы - на моей машине требуется около 4300 факторов. Я оставляю подобный ускорение haskell заинтересованному читателю.

  • 3
    Для сравнения: оригинальная версия c: 9.1690, версия thaumkid: улучшение 0.1060 86x.
  • 0
    Вот это да. Haskell отлично работает, когда вы избегаете выводимых типов
Показать ещё 2 комментария
13
Вопрос 1: освобождают ли erlang, python и haskell скорость из-за использования произвольных целых чисел или они не до тех пор, пока значения меньше MAXINT?

Это маловероятно. Я не могу сказать много о Erlang и Haskell (ну, может быть, немного о Haskell ниже), но я могу указать множество других узких мест в Python. Каждый раз, когда программа пытается выполнить операцию с некоторыми значениями в Python, она должна проверить, соответствуют ли значения правильному типу, и это занимает немного времени. Ваша функция factorCount просто выделяет список с range (1, isquare + 1) разное время, а время выполнения, malloc - выделение выделенной памяти происходит медленнее, чем итерация в диапазоне с помощью счетчика, как и в C. Примечательно, что factorCount() называется многократно и поэтому выделяет много списков. Кроме того, давайте не будем забывать, что Python интерпретируется, и интерпретатор CPython не имеет большого интереса к оптимизации.

EDIT: oh, хорошо, я отмечаю, что вы используете Python 3, поэтому range() не возвращает список, а генератор. В этом случае мой вопрос о распределении списков является неполным: функция просто выделяет range объекты, которые все же неэффективны, но не так неэффективны, как распределение списка с большим количеством элементов.

Вопрос 2: Почему haskell так медленно? Есть ли флаг компилятора, который отключает тормоза или это моя реализация? (Последнее вполне вероятно, так как haskell - это книга с семью печатями для меня.)

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

Вопрос 3: Можете ли вы предложить мне несколько советов по оптимизации этих реализаций без изменения способа определения факторов? Оптимизация в любом случае: лучше, быстрее, более "родной" для языка.

Я бы сказал, что вы играете в беззаботную игру. Наилучшая часть знания разных языков - использовать их по-разному. Но я отвлекаюсь, у меня просто нет рекомендаций по этому вопросу. Извините, я надеюсь, что кто-то может помочь вам в этом случае:)

Вопрос 4. Могут ли мои функциональные реализации разрешить LCO и, следовательно, избежать добавления ненужных кадров в стек вызовов?

Насколько я помню, вам просто нужно убедиться, что ваш рекурсивный вызов является последней командой перед возвратом значения. Другими словами, такая функция, как приведенная ниже, может использовать такую ​​оптимизацию:

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

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

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

Я разделил операции в некоторых локальных переменных, чтобы выяснить, какие операции выполняются. Однако наиболее обычным является просмотр этих функций, как показано ниже, но они эквивалентны для той точки, которую я делаю:

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

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

Обратите внимание, что решение компилятора/интерпретатора должно решить, будет ли он выполнять рекурсию хвоста. Например, интерпретатор Python не делает этого, если я хорошо помню (я использовал Python в моем примере только из-за его свободного синтаксиса). В любом случае, если вы найдете странные вещи, такие как факториальные функции с двумя параметрами (и один из параметров имеет такие имена, как acc, accumulator и т.д.), Теперь вы знаете, почему люди это делают:)

  • 0
    Отлично, наконец, кто-то, кто отвечает на вопросы. Мне нужно будет прочитать ваш ответ более подробно, и я обязательно вернусь с вопросами. Спасибо.
  • 0
    @ Hyperboreus спасибо! Кроме того, мне действительно интересно узнать ваши следующие вопросы. Однако я предупреждаю вас, что мои знания ограничены, поэтому я не смог ответить на все ваши вопросы. Для того, чтобы попытаться компенсировать это, я сделал свой ответ вики-сообществу, чтобы людям было легче дополнить его.
Показать ещё 1 комментарий
11

С 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. Я знаю, что это старый вопрос, и я наткнулся на него из поисковых запросов (я забыл, что я искал, теперь...). Просто хотел прокомментировать вопрос о LCO и выразить свои чувства по поводу Haskell в целом. Я хотел прокомментировать верхний ответ, но комментарии не позволяют блоки кода.

8

Еще несколько номеров и объяснений для версии C. По-видимому, никто не делал этого в течение всех этих лет. Помните, чтобы поддержать этот ответ, чтобы он мог подняться на вершину, чтобы все могли видеть и учиться.

Шаг первый: контрольный пример авторских программ

Характеристики ноутбука:

  • CPU i3 M380 (931 МГц - максимальный режим экономии батареи)
  • 4 ГБ памяти
  • Win7 64 бит
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin с gcc 4.9.3
  • Python 2.7.10

Команды

compiling on VS x64 command prompt > `for /f %f in ('dir /b *.c') do cl /O2 /Ot /Ox %f -o %f_x64_vs2012.exe`
compiling on cygwin with gcc x64   > `for f in ./*.c; do gcc -m64 -O3 $f -o ${f}_x64_gcc.exe ; done`
time (unix tools) using cygwin > `for f in ./*.exe; do  echo "----------"; echo $f ; time $f ; done`

.

----------
$ time python ./original.py

real    2m17.748s
user    2m15.783s
sys     0m0.093s
----------
$ time ./original_x86_vs2012.exe

real    0m8.377s
user    0m0.015s
sys     0m0.000s
----------
$ time ./original_x64_vs2012.exe

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

real    0m20.951s
user    0m20.732s
sys     0m0.030s

Имена файлов: integertype_architecture_compiler.exe

  • integertype - это то же самое, что и исходная программа (подробнее об этом позже)
  • архитектура - это x86 или x64 в зависимости от настроек компилятора.
  • компилятор - gcc или vs2012

Шаг второй: исследовать, улучшать и тестировать снова

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 from #include <stdint.h>

В C есть несколько значений целочисленных типов, а также некоторые подписанные/неподписанные для воспроизведения, плюс выбор для компиляции как x86 или x64 (чтобы не путать с фактическим размером целого). Это много версий для компиляции и запуска ^ ^

Шаг третий: понимание чисел

Окончательные выводы:

  • 32-битные целые числа ~ 200% быстрее, чем 64-разрядные эквиваленты
  • unsigned 64 бит целые числа на 25% быстрее, чем подписанные 64 бит (к сожалению, у меня нет объяснений)

Вопрос об уловке: "Каковы размеры int и long в C?" Правильный ответ: Размер int и long в C не определены корректно!

Из спецификации C:

int не менее 32 бит
  long - это хотя бы int

На странице gcc man (-m32 и -m64 флаги):

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

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

int, 4 байта, также известен как подписанный
  long, 4 байта, также знает как long int и long long int

Завершить это: извлеченные уроки

  • 32-битные целые числа превосходят 64-битные целые числа.

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

  • Проблемы с производительностью решены. Все остальные языки на сотни процентов отстают, C и С++ снова выигрывают! Они всегда это делают. Следующим улучшением будет многопоточность с использованием OpenMP: D

  • 0
    Из любопытства, как это делают компиляторы Intel? Они обычно действительно хороши в оптимизации числового кода.
  • 0
    Где вы найдете ссылку, в которой говорится, что спецификация C гарантирует, что int не менее 32 бит? Единственные известные мне гарантии - это минимальные значения INT_MIN и INT_MAX (-32767 и 32767, которые практически накладывают требование, чтобы int было не менее 16 бит). long должен быть по крайней мере таким же большим, как int , а требования к диапазону означают, что long составляет как минимум 32 бита.
Показать ещё 1 комментарий
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 есть значительные накладные расходы при запуске!

  • 0
    Я забыл упомянуть об этом в своем посте, но я измерил время, необходимое для запуска системы (erl -noshell -s erlang halt) - около 0,1 секунды на моей машине. Это достаточно мало по сравнению со временем выполнения программы (около 10 секунд), о котором не стоит спорить.
  • 0
    на твоей машине! мы не знаем, работаете ли вы на сервере fire fire !. Поскольку время является переменной величиной, пропорциональной характеристикам машины, это следует принимать во внимание….
Показать ещё 1 комментарий
7

Вопрос 1: Устраняет ли Erlang, Python и Haskell скорость из-за использования произвольные длины целых чисел или не до тех пор, пока значения меньше чем MAXINT?

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

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

Так как это быстрее, чем ваш начальный пример C, я бы предположил, что есть многочисленные проблемы, о которых другие подробно рассмотрели.

Этот модуль Erlang выполняется на дешевом нетбуке примерно через 5 секунд... Он использует модель сетевых потоков в erlang и, как таковая, демонстрирует, как использовать модель события. Он может быть распределен по многим узлам. И это быстро. Не мой код.

-module(p12dist).  
-author("Jannich Brendle, [email protected], http://blog.bredsaal.dk").  
-compile(export_all).

server() ->  
  server(1).

server(Number) ->  
  receive {getwork, Worker_PID} -> Worker_PID ! {work,Number,Number+100},  
  server(Number+101);  
  {result,T} -> io:format("The result is: \~w.\~n", [T]);  
  _ -> server(Number)  
  end.

worker(Server_PID) ->  
  Server_PID ! {getwork, self()},  
  receive {work,Start,End} -> solve(Start,End,Server_PID)  
  end,  
  worker(Server_PID).

start() ->  
  Server_PID = spawn(p12dist, server, []),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]).

solve(N,End,_) when N =:= End -> no_solution;

solve(N,End,Server_PID) ->  
  T=round(N*(N+1)/2),
  case (divisor(T,round(math:sqrt(T))) > 500) of  
    true ->  
      Server_PID ! {result,T};  
    false ->  
      solve(N+1,End,Server_PID)  
  end.

divisors(N) ->  
  divisor(N,round(math:sqrt(N))).

divisor(_,0) -> 1;  
divisor(N,I) ->  
  case (N rem I) =:= 0 of  
  true ->  
    2+divisor(N,I-1);  
  false ->  
    divisor(N,I-1)  
  end.

Тест был проведен на: Intel (R) Atom (TM) CPU N270 @1,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
  • 0
    увеличение значения до 1000, как показано ниже, не дает правильного результата. С> 500, как указано выше, новейший тест: процессор IntelCore2 6600 @ 2,40 ГГц завершает работу в режиме реального времени 0m2.370s
  • 0
    ваш результат: 76576500 все остальные: 842161320 что-то не так с вашим результатом
Показать ещё 2 комментария
3

Попытка GO:

package main

import "fmt"
import "math"

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

Я получаю:

оригинальная версия c: 9.1690 100%
go: 8.2520 111%

Но используя:

package main

import (
    "math"
    "fmt"
 )

// Sieve of Eratosthenes
func PrimesBelow(limit int) []int {
    switch {
        case limit < 2:
            return []int{}
        case limit == 2:
            return []int{2}
    }
    sievebound := (limit - 1) / 2
    sieve := make([]bool, sievebound+1)
    crosslimit := int(math.Sqrt(float64(limit))-1) / 2
    for i := 1; i <= crosslimit; i++ {
        if !sieve[i] {
            for j := 2 * i * (i + 1); j <= sievebound; j += 2*i + 1 {
                sieve[j] = true
            }
        }
    }
    plimit := int(1.3*float64(limit)) / int(math.Log(float64(limit)))
    primes := make([]int, plimit)
    p := 1
    primes[0] = 2
    for i := 1; i <= sievebound; i++ {
        if !sieve[i] {
            primes[p] = 2*i + 1
            p++
            if p >= plimit {
                break
            }
        }
    }
    last := len(primes) - 1
    for i := last; i > 0; i-- {
        if primes[i] != 0 {
            break
        }
        last = i
    }
    return primes[0:last]
}



func main() {
    fmt.Println(p12())
}
// Requires PrimesBelow from utils.go
func p12() int {
    n, dn, cnt := 3, 2, 0
    primearray := PrimesBelow(1000000)
    for cnt <= 1001 {
        n++
        n1 := n
        if n1%2 == 0 {
            n1 /= 2
        }
        dn1 := 1
        for i := 0; i < len(primearray); i++ {
            if primearray[i]*primearray[i] > n1 {
                dn1 *= 2
                break
            }
            exponent := 1
            for n1%primearray[i] == 0 {
                exponent++
                n1 /= primearray[i]
            }
            if exponent > 1 {
                dn1 *= exponent
            }
            if n1 == 1 {
                break
            }
        }
        cnt = dn * dn1
        dn = dn1
    }
    return n * (n - 1) / 2
}

Я получаю:

оригинальная версия c: 9.1690 100%
thaumkid c версия: 0.1060 8650%
первая версия go: 8.2520 111%
второй вариант go: 0.0230 39865%

Я также попробовал Python3.6 и pypy3.3-5.5-alpha:

оригинальная версия c: 8.629 100%
thaumkid c версия: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

а затем со следующим кодом, который я получил:

оригинальная версия c: 8.629 100%
thaumkid c версия: 0.109 8650%
Python3.6: 1.489 580%
pypy3.3-5.5-alpha: 0,582 1483%

def D(N):
    if N == 1: return 1
    sqrtN = int(N ** 0.5)
    nf = 1
    for d in range(2, sqrtN + 1):
        if N % d == 0:
            nf = nf + 1
    return 2 * nf - (1 if sqrtN**2 == N else 0)

L = 1000
Dt, n = 0, 0

while Dt <= L:
    t = n * (n + 1) // 2
    Dt = D(n/2)*D(n+1) if n%2 == 0 else D(n)*D((n+1)/2)
    n = n + 1

print (t)
3

С++ 11, 20 мс для меня - Запустить его здесь

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

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

Этот код использует только пару (уродливых) оптимизаций, не связанных с используемым языком, на основе:

  • каждый номер троицы имеет вид n (n + 1)/2
  • n и n + 1 являются взаимно простыми
  • число делителей является мультипликативной функцией

#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
    Это явно не то, что просил спрашивающий: «Я действительно пытался реализовать один и тот же алгоритм, максимально похожий на четырех языках». Процитирую комментарий к одному из многих удаленных ответов, похожих на ваш, «совершенно очевидно, что вы можете получить более высокие скорости с лучшим алгоритмом независимо от языка».
  • 2
    @ ThomasM.DuBuisson. Вот к чему я клоню. Вопрос \ ответы в значительной степени подразумевают, что алгоритмические ускорения являются значительными (и, конечно, OP их не запрашивает), но явного примера нет. Я думаю, что этот ответ - который не совсем оптимизированный код - предоставляет немного полезного контекста для любого, как я, который задавался вопросом, насколько медленным / быстрым был код OP.
Показать ещё 3 комментария
1

Я сделал предположение, что число факторов велико только в том случае, если задействованные числа имеют много мелких факторов. Поэтому я использовал отличный алгоритм thaumkid, но сначала использовал приближение к счету факторов, который никогда не бывает слишком маленьким. Это довольно просто: проверьте, чтобы простые коэффициенты до 29, затем проверяйте оставшееся число и вычисляйте верхнюю границу для nber факторов. Используйте это, чтобы вычислить верхнюю оценку числа факторов, и если это число достаточно велико, вычислите точное количество факторов.

В приведенном ниже коде не нужно это предположение для правильности, но чтобы быть быстрым. Кажется, это работает; только около одного из 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-го треугольного номера с 172 032 фактора за 21 минуту 25 секунд (2,4 ГГц Core2 Duo), поэтому этот код занимает в среднем всего 116 циклов процессора на число. Последнее самое треугольное число больше 2 ^ 68, поэтому

1

Изменить: case (divisor(T,round(math:sqrt(T))) > 500) of

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

Это даст правильный ответ для примера многопроцесса Erlang.

  • 2
    Это было задумано как комментарий к этому ответу ? Потому что это не ясно, и это не ответ сам по себе.
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 всего

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$
-4

Самый быстрый язык программирования "высокого уровня" - fortran. Он используется для тестирования тестов на суперкомпьютерах и очень быстро при умножении матрицы и так далее. Я бы посоветовал вам использовать cython, это иногда даже быстрее, чем C, и намного проще кодировать, потому что пусть сталкивается с этим. Python - это лучший язык для кода. Надеюсь, я немного помогу тебе.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню