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

я взял #12 С Проект Эйлера в качестве упражнения по программированию и для сравнения моих (конечно, не оптимальных) реализаций в 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

Python:

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

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

Python с PyPy:

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

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

официальный сайт

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

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

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

Хаскелл:

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

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

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

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

EDIT:

Вопрос 4: Разрешают ли мои функциональные реализации LCO (оптимизация последнего вызова, 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 ("%ldn", 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

18 ответов


используя GHC 7.0.3, gcc 4.4.6, Linux 2.6.29 на машине x86_64 Core2 Duo (2.5 GHz), компиляция с использованием ghc -O2 -fllvm -fforce-recomp для Haskell и gcc -O3 -lm С.

  • ваша подпрограмма C выполняется за 8,4 секунды (быстрее, чем ваш запуск, вероятно, из-за -O3)
  • решение Haskell выполняется за 36 секунд (из-за -O2 флаг)
  • код factorCount' код явно не набран и по умолчанию не Integer (спасибо Даниэлю за исправление моего неправильного диагноза здесь!). Дающий явная подпись типа (что в любом случае является стандартной практикой) с использованием Int и время меняется на 11,1 секунды
  • на factorCount' вы напрасно называют fromIntegral. Исправление не приводит к изменениям (компилятор умный, к счастью для вас).
  • используется mod здесь rem быстрее и достаточными. Это изменяет время на 8,5 секунд.
  • factorCount' постоянно применяет два дополнительных аргумента, которые никогда изменение (number, sqrt). Преобразование worker/wrapper дает нам:
 $ 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 скорость из-за использования произвольные целые числа длины или нет, пока значения меньше чем МАКСИНТ?

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

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

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

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

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

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

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


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

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

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

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

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

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

что не так уж плохо по сравнению с 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 верен в отношении оптимизации последнего вызова.


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


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

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

вот измененный код (просто заменить долго С int и я явно inlined 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 обычно генерирует более быстрый код.


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

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


ваша реализация 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 { bin2_slow.exe }

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

улучшена реализация

PS> measure-command { bin2.exe }

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

как вы можете видеть, этот работает за 38 миллисекунд на той же машине, где ваш побежал за 16 секунд:)

компиляция команды:

ghc -O2 012.hs -o bin2.exe
ghc -O2 012_slow.hs -o bin2_slow.exe

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

изменение лонгов на ints в main, используя gcc -O3 -lm, это последовательно работает в 0.31-0.35 секунды.

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

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

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


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

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

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

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

вы используете обнимашки? 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 etc.) теперь вы знаете, почему люди это делают:)


С 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.49 s) о 6% медленнее, чем версия в ответе @Thomas (~ 7.04 s) на моей машине с GHC 7.6.2, в то время как версия C от @Raedwulf работает ~ 3.15 s. Кажется, GHC улучшилось за год.

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


глядя на вашу реализацию 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 имеет значительные накладные расходы при запуске!


еще несколько чисел и объяснений для версии 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 С #include <stdint.h>

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

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

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

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

хитрый вопрос: "каковы размеры int и long в C?"
Право ответ: размер int и long в C не определены!

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

int составляет не менее 32 бит
долго не менее инт

из справочной страницы gcc (флаги-M32 и-M64):

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

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

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

В Заключение: Извлеченные Уроки

  • 32-битные целые числа быстрее, чем 64-битные целые числа.

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

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

Вопрос 1: теряют ли Erlang, Python и Haskell скорость из-за использования произвольные целые числа длины или нет, пока значения меньше чем МАКСИНТ?

на вопрос можно ответить отрицательно для Эрланга. Последний вопрос ответил правильно на языке Erlang, как в:

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

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

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

-module(p12dist).  
-author("Jannich Brendle, jannich@bredsaal.dk, http://blog.bredsaal.dk").  
-compile(export_all).

server() ->  
  server(1).

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

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

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

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

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

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

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

тест ниже состоялся на: Intel(R) Atom (TM) CPU N270 @ 1.60 GHz

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

в C++11, - запустить его здесь

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

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

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

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

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

using namespace std;

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

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

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

        divisors_count *= (divisions + 1);

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

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

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

    n = 1; n_p_1 = 2;

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

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

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

        //the first (unwound) iteration

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

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

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

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

        //now the second (unwound) iteration

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

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

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

    return (n * n_p_1) / 2;
}

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

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

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

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


попытка 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%
версия C thaumkid: 0.1060 8650%
первая версия go: 8.2520 111%
вторая версия go: 0.0230 39865%

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

оригинальная версия c: 8.629 100%
версия C thaumkid: 0.109 7916%
Питон3.6: 54.795 16%
pypy3.3-5.5-альфа: 13.291 65%

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

оригинальная версия c: 8.629 100%
версия C thaumkid: 0.109 8650%
Питон3.6: 1.489 580%
pypy3.3-5.5-альфа: 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)

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

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

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


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

приведенный ниже код не нуждается в этом предположении для корректности, но должен быть быстрым. Кажется, это работает; только около одного из 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, поэтому


Я изменил версию "Jannich Brendle" на 1000 вместо 500. И перечислите результат euler12.bin, euler12.erl, p12dist.Эрль. Оба кода 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$

#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 Эйлер.c

времени ./а.из

2.79 s пользователь 0.00 s система 99% cpu 2.794 всего