Comparação de velocidade com Project Euler: C vs Python vs Erlang vs Haskell

672

Peguei o Problema 12 do Project Euler como um exercício de programação e comparei minhas (certamente não ótimas) implementações em C, Python, Erlang e Haskell. Para obter tempos de execução mais altos, procuro o primeiro número do triângulo com mais de 1000 divisores em vez de 500, conforme indicado no problema original.

O resultado é o seguinte:

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

Pitão:

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

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

Python com 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

Resumo:

  • C: 100%
  • Python: 692% (118% com PyPy)
  • Erlang: 436% (135% graças a RichardC)
  • Haskell: 1421%

Suponho que C tenha uma grande vantagem, pois usa muito para os cálculos e não inteiros de comprimento arbitrário como os outros três. Além disso, ele não precisa carregar um tempo de execução primeiro (os outros?).

Pergunta 1: Erlang, Python e Haskell perdem velocidade devido ao uso de números inteiros de comprimento arbitrário ou não, desde que os valores sejam menores que MAXINT?

Pergunta 2: Por que o Haskell é tão lento? Existe um sinalizador de compilador que desliga os freios ou é minha implementação? (O último é bastante provável, pois Haskell é um livro com sete selos para mim.)

Pergunta 3: Você pode me oferecer algumas dicas de como otimizar essas implementações sem alterar a maneira como determino os fatores? Otimização de qualquer forma: mais agradável, mais rápido, mais "nativo" para o idioma.

EDITAR:

Pergunta 4: Minhas implementações funcionais permitem o LCO (otimização da última chamada, também conhecida como eliminação da recursão da cauda) e, portanto, evitam adicionar quadros desnecessários à pilha de chamadas?

Eu realmente tentei implementar o mesmo algoritmo o mais semelhante possível nas quatro línguas, embora eu tenha que admitir que meu conhecimento de Haskell e Erlang é muito limitado.


Códigos-fonte usados:

#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
Hyperboreus
fonte
55
@Jochen (e Seth) Não é realmente que C seja rápido ou impressionante, mas é percebido como fácil escrever código de desempenho (que pode não ser verdade, mas a maioria dos programas parece capaz, tão verdadeira o suficiente). À medida que exploro em minha resposta e descobri que isso é verdade ao longo do tempo, a habilidade do programador e o conhecimento de otimizações comuns para o idioma escolhido são de grande importância (especialmente para Haskell).
Thomas M. DuBuisson
52
Verifiquei apenas com Mathematica - que leva 0.25sec (com C leva 6sec aqui), eo código é apenas: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. viva!
tsvikas 8/08/11
35
Existe mais alguém lá fora que se lembre dessas guerras entre C e assembléia? "Claro! Você pode escrever seu código 10x mais rápido em C, mas seu código C pode ser executado tão rápido? ..." Tenho certeza de que as mesmas batalhas foram travadas entre código de máquina e montagem.
JS.
39
@JS: Provavelmente não, pois o assembly é simplesmente um conjunto de mnemônicos que você digita em vez do código de máquina binário bruto - normalmente há uma correspondência de 1 a 1 entre eles.
Callum Rogers
9
a conclusão, para Haskell: -O2 fornece cerca de 3x uma aceleração e, usando Int em vez de Inteiro, cerca de 4x-6x para a aceleração total de 12x-14x e mais.
Will Ness

Respostas:

794

Usando GHC 7.0.3, gcc 4.4.6, Linux 2.6.29em um Core 2 Duo (2,5 GHz) máquina x86_64, compilar utilizando ghc -O2 -fllvm -fforce-recomppara Haskell e gcc -O3 -lmpara C.

  • Sua rotina C é executada em 8,4 segundos (mais rápido que a sua execução provavelmente por causa de -O3)
  • A solução Haskell é executada em 36 segundos (devido ao -O2sinalizador)
  • Seu factorCount'código não está explicitamente digitado e padronizado Integer(obrigado Daniel por corrigir meu erro de diagnóstico aqui!). Fornecer uma assinatura de tipo explícita (que é uma prática padrão de qualquer maneira) usando Inte o tempo muda para 11,1 segundos
  • em factorCount'você chamou desnecessariamente fromIntegral. Uma correção resulta em nenhuma alteração (o compilador é inteligente, tem sorte para você).
  • Você usou modonde remé mais rápido e suficiente. Isso altera o tempo para 8,5 segundos .
  • factorCount'está constantemente aplicando dois argumentos extras que nunca mudam ( number, sqrt). Uma transformação de trabalhador / invólucro nos fornece:
 $ time ./so
 842161320  

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

Isso mesmo, 7,95 segundos . Consistentemente meio segundo mais rápido do que a solução C . Sem a -fllvmbandeira que ainda estou recebendo 8.182 seconds, o backend do NCG também está indo bem neste caso.

Conclusão: Haskell é incrível.

Código resultante

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: Então, agora que exploramos isso, vamos abordar as perguntas

Pergunta 1: Erlang, python e haskell perdem velocidade devido ao uso de números inteiros de comprimento arbitrário ou não, desde que os valores sejam menores que MAXINT?

Em Haskell, o uso Integeré mais lento do que o Intquanto mais lento depende dos cálculos realizados. Felizmente (para máquinas de 64 bits) Inté suficiente. Por questões de portabilidade, você provavelmente deve reescrever meu código para usar Int64ou Word64(C não é o único idioma com a long).

Pergunta 2: Por que o haskell é tão lento? Existe um sinalizador de compilador que desliga os freios ou é minha implementação? (O último é bastante provável, pois haskell é um livro com sete selos para mim.)

Pergunta 3: Você pode me oferecer algumas dicas de como otimizar essas implementações sem alterar a maneira como determino os fatores? Otimização de qualquer forma: mais agradável, mais rápido, mais "nativo" para o idioma.

Foi isso que eu respondi acima. A resposta foi

  • 0) Use otimização via -O2
  • 1) Use tipos rápidos (principalmente: não habilitados para caixa) quando possível
  • 2) remnão mod(uma otimização frequentemente esquecida) e
  • 3) transformação de trabalhador / invólucro (talvez a otimização mais comum).

Pergunta 4: Minhas implementações funcionais permitem LCO e, portanto, evitam adicionar quadros desnecessários à pilha de chamadas?

Sim, esse não era o problema. Bom trabalho e feliz por você ter considerado isso.

Thomas M. DuBuisson
fonte
25
@Karl Porque remna verdade é um subcomponente da modoperação (eles não são os mesmos). Se você procurar na biblioteca do GHC Base, verá modtestes para várias condições e ajusta o sinal de acordo. (ver modInt#em Base.lhs)
Thomas M. DuBuisson
20
Outro dado: escrevi uma tradução rápida de Haskell do programa C sem olhar para o Haskell do @ Hyperboreus. Portanto, é um pouco mais próximo do Haskell idiomático padrão, e a única otimização que adicionei deliberadamente é substituída modpor remdepois de ler esta resposta (heh, oops). Veja o link para meus horários, mas a versão curta é "quase idêntica à C".
CA McCann
106
Mesmo que a versão C tenha sido mais rápida na minha máquina, tenho um novo respeito por Haskell agora. +1
Seth Carnegie
11
Isso é bastante surpreendente para mim, embora eu ainda não tenha experimentado. Como o original factorCount'era recursivo de cauda, ​​eu pensaria que o compilador pudesse identificar os parâmetros extras que não estavam sendo alterados e otimizar a recursão de cauda apenas para os parâmetros alterados (Haskell sendo uma linguagem pura, afinal, isso deve ser fácil). Alguém acha que o compilador poderia fazer isso ou devo voltar a ler mais artigos teóricos?
precisa saber é o seguinte
22
@ kizzx2: Existe um ticket do GHC para adicioná-lo. Pelo que entendi, essa transformação pode resultar em alocações adicionais de objetos de fechamento. Isso significa pior desempenho em alguns casos, mas, como sugere Johan Tibell em seu blog, isso pode ser evitado se o wrapper resultante puder ser incorporado.
Hammar
224

Existem alguns problemas com a implementação do Erlang. Como linha de base para o seguinte, meu tempo de execução medido para o seu programa Erlang não modificado foi de 47,6 segundos, comparado a 12,7 segundos para o código C.

A primeira coisa que você deve fazer se desejar executar o código Erlang intensivamente computacional é usar o código nativo. Compilar com erlc +native euler12reduziu o tempo para 41,3 segundos. No entanto, essa é uma aceleração muito menor (apenas 15%) do que o esperado na compilação nativa nesse tipo de código, e o problema é seu uso -compile(export_all). Isso é útil para experimentação, mas o fato de todas as funções serem potencialmente acessíveis externamente faz com que o compilador nativo seja muito conservador. (O emulador BEAM normal não é muito afetado.) Substituir esta declaração -export([solve/0]).fornece uma aceleração muito melhor: 31,5 segundos (quase 35% da linha de base).

Mas o código em si tem um problema: para cada iteração no loop factorCount, você executa este teste:

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

O código C não faz isso. Em geral, pode ser complicado fazer uma comparação justa entre diferentes implementações do mesmo código e, em particular, se o algoritmo for numérico, porque você precisa ter certeza de que eles estão realmente fazendo a mesma coisa. Um ligeiro erro de arredondamento em uma implementação devido a alguma conversão de texto em algum lugar pode fazer com que ele faça muito mais iterações do que a outra, mesmo que ambas cheguem ao mesmo resultado.

Para eliminar essa possível fonte de erro (e se livrar do teste extra em cada iteração), reescrevi a função factorCount da seguinte maneira, modelada de perto no código 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.

Essa reescrita, não export_alle compilação nativa me deu o seguinte tempo de execução:

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

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

o que não é tão ruim se comparado ao código C:

$ time ./a.out 
842161320

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

considerando que Erlang não é voltado para escrever código numérico, sendo apenas 50% mais lento que C em um programa como esse é muito bom.

Por fim, em relação às suas perguntas:

Pergunta 1: Erlang, python e haskell perdem velocidade devido ao uso de números inteiros de comprimento arbitrário ou não, desde que os valores sejam menores que MAXINT?

Sim, um pouco. Em Erlang, não há como dizer "use aritmética de 32/64 bits com wrap-around", portanto, a menos que o compilador possa provar alguns limites em seus números inteiros (e geralmente não), ele deve verificar todos os cálculos para ver se eles podem caber em uma única palavra marcada ou se precisar transformá-los em bignums alocados em heap. Mesmo que nenhum bignum seja usado na prática em tempo de execução, essas verificações precisarão ser executadas. Por outro lado, isso significa que você sabe que o algoritmo nunca falhará por causa de um número inteiro inesperado se você repentinamente fornecer entradas maiores do que antes.

Pergunta 4: Minhas implementações funcionais permitem LCO e, portanto, evitam adicionar quadros desnecessários à pilha de chamadas?

Sim, seu código Erlang está correto em relação à otimização da última chamada.

RichardC
fonte
2
Eu concordo com você. Esta referência não era preciso especialmente para Erlang para uma série de razões
Muzaaya Joshua
156

No que diz respeito à otimização do Python, além de usar o PyPy (para acelerações impressionantes e sem alterações no código), você pode usar a cadeia de ferramentas de tradução do PyPy para compilar uma versão compatível com RPython ou o Cython para criar um módulo de extensão, ambos que são mais rápidos que a versão C nos meus testes, com o módulo Cython quase o dobro da velocidade . Para referência, incluí também os resultados de benchmark C e PyPy:

C (compilado com 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 (usando a revisão mais recente do 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

A versão do RPython possui algumas alterações importantes. Para traduzir para um programa independente, você precisa definir o seu target, que nesse caso é a mainfunção. Espera-se que aceite sys.argvcomo único argumento e é necessário retornar um int. Você pode traduzi-lo usando translate.py, % translate.py euler12-rpython.pyque é traduzido para C e o compila para você.

# 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

A versão do Cython foi reescrita como um módulo de extensão _euler12.pyx, que eu importo e chamo de um arquivo python normal. O _euler12.pyxé essencialmente o mesmo que a sua versão, com algumas declarações adicionais tipo estático. O setup.py possui o padrão normal para criar a extensão, usando 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
)

Sinceramente, tenho muito pouca experiência com RPython ou Cython, e fiquei agradavelmente surpreso com os resultados. Se você estiver usando o CPython, escrever os bits de código com muita CPU em um módulo de extensão Cython parece uma maneira muito fácil de otimizar seu programa.

zeekay
fonte
6
Estou curioso, a versão C pode ser otimizada para ser pelo menos tão rápida quanto o CPython?
Nome para exibição
4
@SargeBorsch essa versão Cython é tão rápido, porque ele compila para baixo a uma fonte altamente otimizado C, o que significa que com certeza você pode obter esse desempenho de C.
Eli Korvigo
72

Pergunta 3: Você pode me oferecer algumas dicas de como otimizar essas implementações sem alterar a maneira como determino os fatores? Otimização de qualquer forma: mais agradável, mais rápido, mais "nativo" para o idioma.

A implementação C é abaixo do ideal (como sugerido por Thomas M. DuBuisson), a versão usa números inteiros de 64 bits (ou seja, tipo de dados longo ). Investigarei a listagem de montagem mais tarde, mas com um palpite, há alguns acessos à memória no código compilado, o que torna o uso de números inteiros de 64 bits significativamente mais lento. É esse ou o código gerado (o fato de que você pode ajustar menos ints de 64 bits em um registro SSE ou arredondar um dobro para um número inteiro de 64 bits é mais lento).

Aqui está o código modificado (basta substituir long por int e eu expliquei explicitamente factorCount, embora eu não ache que isso seja necessário com 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);
}

Correr + cronometrar:

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

Para referência, a implementação de haskell por Thomas na resposta anterior fornece:

$ 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

Conclusão: Tirando nada do ghc, é um ótimo compilador, mas o gcc normalmente gera código mais rápido.

Raedwulf
fonte
22
Muito agradável! Para comparação, na minha máquina, sua solução C é executada 2.5 secondsenquanto uma modificação semelhante ao código Haskell (movendo-se para o Word32, adicionando pragma INLINE) resulta em um tempo de execução de 4.8 seconds. Talvez algo possa ser feito (não trivialmente, ao que parece) - o resultado do gcc é certamente impressionante.
Thomas M. DuBuisson
1
Obrigado! Talvez a questão deva ser a velocidade da saída compilada por vários compiladores, e não a própria linguagem. Por outro lado, retirar os manuais da Intel e otimizar manualmente ainda será vitorioso (desde que você tenha o conhecimento e o tempo (muito)).
Raedwulf 16/08
56

Dê uma olhada neste blog . Nos últimos anos, ele resolveu alguns dos problemas do Project Euler em Haskell e Python e, em geral, descobriu que Haskell era muito mais rápido. Eu acho que entre essas linguagens isso tem mais a ver com sua fluência e estilo de codificação.

Quando se trata de velocidade do Python, você está usando a implementação errada! Experimente o PyPy e, para coisas como essa, você descobrirá que é muito, muito mais rápido.

agf
fonte
32

Sua implementação do Haskell pode ser bastante acelerada usando algumas funções dos pacotes Haskell. Nesse caso, usei primes, que são instalados apenas com '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)

Horários:

Seu programa original:

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

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Implementação aprimorada

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

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Como você pode ver, este é executado em 38 milissegundos na mesma máquina em que o seu foi executado em 16 segundos :)

Comandos de compilação:

ghc -O2 012.hs -o bin\012.exe
ghc -O2 012_slow.hs -o bin\012_slow.exe
Connie Hilarides
fonte
5
A última vez que verifiquei Haskell "primos" era apenas uma enorme lista de primos pré-computados - sem cálculo, apenas pesquisa. Então, sim, é claro que isso será mais rápido, mas não diz nada sobre a velocidade computacional de derivar números primos em Haskell.
Zxq9
21
@ zxq9, você poderia me indicar onde, na fonte do pacote primes ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ), está localizada a lista de números primos?
Fraser
4
Embora a fonte mostre que os números primos não são pré-computados, essa velocidade é totalmente insana, milhas mais rápidas que a versão C, então o que diabos está acontecendo?
ponto
1
memorização @semicolon. Nesse caso, acho que Haskell memorizou todos os primos em tempo de execução, para que não precise recalculá-los a cada iteração.
Hauleth 30/03/16
5
É 1000 divisores, não 500.
Casper Færgemand
29

Apenas por diversão. A seguir, uma implementação Haskell mais 'nativa':

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

Usando ghc -O3, isso é executado de forma consistente em 0,55-0,58 segundos na minha máquina (Core i7 de 1,73 GHz).

Uma função factorCount mais eficiente para a versão 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;
}

Alterar longs para ints em main, usando gcc -O3 -lm, isso é executado consistentemente em 0,31-0,35 segundos.

Ambos podem ser executados ainda mais rapidamente se você tirar proveito do fato de que o n-ésimo número do triângulo = n * (n + 1) / 2 e n e (n + 1) possuem fatorações primárias completamente díspares, portanto, o número de fatores de cada metade pode ser multiplicado para encontrar o número de fatores do todo. Os seguintes:

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

reduz o tempo de execução do código c para 0,17-0,19 segundos e pode lidar com pesquisas muito maiores - mais de 10000 fatores levam cerca de 43 segundos na minha máquina. Deixo uma aceleração de haskell semelhante para o leitor interessado.

thaumkid
fonte
3
Apenas para comparação: versão original c: 9.1690, versão thaumkid: 0.1060 86x melhoria.
thanos
Uau. Haskell realiza grande uma vez que você evitar os tipos inferidos
Piyush Katariya
Na verdade, não é a inferência que fez isso. Isso apenas ajuda você A) a depurar ou evitar problemas de tipo e problemas de seleção de instância de classe de tipo B) depurar e evitar alguns problemas de tipo indecidíveis com algumas extensões de idioma modernas. Também ajuda a tornar seus programas incomparáveis, para que você nunca possa aumentar seus esforços de desenvolvimento.
codeshot
c versão 0.11 s sobre Intel crânio canyon
codeshot
13
Pergunta 1: Erlang, python e haskell perdem velocidade devido ao uso de números inteiros de comprimento arbitrário ou não, desde que os valores sejam menores que MAXINT?

Isso é improvável. Não posso dizer muito sobre Erlang e Haskell (bem, talvez um pouco sobre Haskell abaixo), mas posso apontar muitos outros gargalos no Python. Toda vez que o programa tenta executar uma operação com alguns valores no Python, deve verificar se os valores são do tipo apropriado e custa um pouco de tempo. Sua factorCountfunção apenas aloca uma lista com range (1, isquare + 1)vários horários, e a mallocalocação de memória com estilo de execução é muito mais lenta do que iterando em um intervalo com um contador, como você faz em C. Notavelmente, isso factorCount()é chamado várias vezes e, portanto, aloca muitas listas. Além disso, não devemos esquecer que o Python é interpretado e o intérprete do CPython não tem grande foco em ser otimizado.

EDIT : oh, bem, notei que você está usando Python 3, portanto range(), não retorna uma lista, mas um gerador. Nesse caso, meu argumento sobre alocar listas está meio errado: a função apenas aloca rangeobjetos, que são ineficientes, mas não tão ineficientes quanto alocar uma lista com muitos itens.

Pergunta 2: Por que o haskell é tão lento? Existe um sinalizador de compilador que desliga os freios ou é minha implementação? (O último é bastante provável, pois haskell é um livro com sete selos para mim.)

Você está usando abraços ? Abraços é um intérprete consideravelmente lento. Se você o estiver usando, talvez você possa se divertir melhor com o GHC - mas eu só estou cogitando de hipoteses, o tipo de coisa que um bom compilador Haskell faz sob o capô é bastante fascinante e está além da minha compreensão :)

Pergunta 3: Você pode me oferecer algumas dicas de como otimizar essas implementações sem alterar a maneira como determino os fatores? Otimização de qualquer forma: mais agradável, mais rápido, mais "nativo" para o idioma.

Eu diria que você está jogando um jogo sem graça. A melhor parte de conhecer vários idiomas é usá-los da maneira mais diferente possível :) Mas discordo, não tenho nenhuma recomendação para esse ponto. Desculpe, espero que alguém possa ajudá-lo neste caso :)

Pergunta 4: Minhas implementações funcionais permitem LCO e, portanto, evitam adicionar quadros desnecessários à pilha de chamadas?

Tanto quanto me lembro, você só precisa ter certeza de que sua chamada recursiva é o último comando antes de retornar um valor. Em outras palavras, uma função como a abaixo poderia usar essa otimização:

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

No entanto, você não teria essa otimização se sua função fosse como a abaixo, porque existe uma operação (multiplicação) após a chamada recursiva:

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

Separei as operações em algumas variáveis ​​locais para deixar claro quais operações são executadas. No entanto, o mais comum é ver essas funções como abaixo, mas elas são equivalentes para o argumento que estou enfatizando:

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

Observe que cabe ao compilador / intérprete decidir se fará recursão final. Por exemplo, o intérprete Python não o faz se me lembro bem (usei o Python no meu exemplo apenas por causa de sua sintaxe fluente). De qualquer forma, se você encontrar coisas estranhas, como funções fatoriais, com dois parâmetros (e um dos parâmetros tiver nomes como acc, accumulatoretc.), agora você sabe por que as pessoas fazem isso :)

brandizzi
fonte
@Hyperboreus thank you! Além disso, estou realmente curioso sobre suas próximas perguntas. No entanto, aviso que meu conhecimento é limitado e não pude responder a todas as suas perguntas. Por tentar compensá-lo, fiz minha wiki da comunidade de respostas, para que as pessoas possam complementá-lo mais facilmente.
Brandizzi
Sobre o uso do intervalo. Quando substituo o intervalo por um loop while com incremento (imitando o loop for de C), o tempo de execução na verdade dobra. Eu acho que os geradores são bastante otimizados.
Hyperboreus
12

Com Haskell, você realmente não precisa pensar em recursões explicitamente.

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

No código acima, substituí recursões explícitas na resposta do @Thomas por operações de lista comuns. O código ainda faz exatamente a mesma coisa sem nos preocuparmos com a recursão da cauda. Ele roda (~ 7.49s ) cerca de 6% mais lento que a versão na resposta do @Thomas (~ 7.04s ) na minha máquina com o GHC 7.6.2, enquanto a versão C do @Raedwulf executa ~ 3.15s . Parece que o GHC melhorou ao longo do ano.

PS. Sei que é uma pergunta antiga e me deparo com ela nas pesquisas do google (esqueci o que estava pesquisando agora ...). Só queria comentar a pergunta sobre LCO e expressar meus sentimentos sobre Haskell em geral. Eu queria comentar a resposta principal, mas os comentários não permitem blocos de código.

jxy
fonte
9

Mais alguns números e explicações para a versão C. Aparentemente, ninguém fez isso durante todos esses anos. Lembre-se de votar acima desta resposta para que ela fique no topo para que todos vejam e aprendam.

Etapa 1: Referência dos programas do autor

Especificações do laptop:

  • CPU i3 M380 (931 MHz - modo de economia de bateria máxima)
  • 4GB de memória
  • Win7 64 bits
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin com o gcc 4.9.3
  • Python 2.7.10

Comandos:

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

Os nomes de arquivos são: integertype_architecture_compiler.exe

  • integertype é o mesmo que o programa original por enquanto (mais sobre isso mais tarde)
  • arquitetura é x86 ou x64, dependendo das configurações do compilador
  • compilador é gcc ou vs2012

Etapa 2: Investigar, melhorar e comparar novamente

O VS é 250% mais rápido que o gcc. Os dois compiladores devem fornecer uma velocidade semelhante. Obviamente, algo está errado com o código ou as opções do compilador. Vamos investigar!

O primeiro ponto de interesse são os tipos inteiros. As conversões podem ser caras e a consistência é importante para uma melhor geração e otimizações de código. Todos os números inteiros devem ser do mesmo tipo.

É uma bagunça mista inte longagora. Nós vamos melhorar isso. Que tipo de usar? O mais rápido. Tenho que avaliar todos eles!

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

Tipos inteiros são int long int32_t uint32_t int64_te uint64_tde#include <stdint.h>

Existem muitos tipos de números inteiros em C, além de alguns assinados / não assinados para brincar, além da opção de compilar como x86 ou x64 (que não deve ser confundido com o tamanho inteiro real). São muitas versões para compilar e executar ^^

Etapa três: Compreendendo os números

Conclusões definitivas:

  • Inteiros de 32 bits são ~ 200% mais rápidos que equivalentes de 64 bits
  • números inteiros de 64 bits não assinados são 25% mais rápidos que 64 bits assinados (infelizmente, não tenho explicação para isso)

Truque de pergunta: "Quais são os tamanhos de int e long em C?"
A resposta certa é: O tamanho de int e long em C não está bem definido!

A partir da especificação C:

int tem pelo menos 32 bits de
comprimento é pelo menos um int

Na página do manual do gcc (sinalizadores -m32 e -m64):

O ambiente de 32 bits define int, long e apontador para 32 bits e gera código que é executado em qualquer sistema i386.
O ambiente de 64 bits define int para 32 bits e long e o ponteiro para 64 bits e gera código para a arquitetura x86-64 da AMD.

Na documentação do MSDN (intervalos de tipos de dados) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 bytes, também conhecido como assinado por
muito tempo, 4 bytes, também conhecido como int e assinado por muito tempo

Para concluir: lições aprendidas

  • Inteiros de 32 bits são mais rápidos que números inteiros de 64 bits.

  • Os tipos inteiros padrão não são bem definidos em C nem C ++, eles variam dependendo dos compiladores e arquiteturas. Quando você precisar de consistência e previsibilidade, use a uint32_tfamília inteira de #include <stdint.h>.

  • Problemas de velocidade resolvidos. Todos os outros idiomas estão de volta centenas por cento atrás, C & C ++ vencem novamente! Eles sempre fazem. A próxima melhoria será multithreading usando o OpenMP: D

user5994461
fonte
Por curiosidade, como os compiladores Intel fazem? Eles geralmente são muito bons em otimizar código numérico.
Kirbyfan64sos
Onde você encontra uma referência dizendo que a especificação C garante "int é pelo menos 32 bits"? As únicas garantias que conheço são as magnitudes mínimas de INT_MINe INT_MAX(-32767 e 32767, que praticamente impõem um requisito de intpelo menos 16 bits). longdeve ser pelo menos tão grande quanto um int, e a média dos requisitos de intervalo longé de pelo menos 32 bits.
ShadowRanger 29/11
Você parece estar correto. stackoverflow.com/questions/1231147/is-int-in-c-always-32-bit
user5994461
8

Olhando para sua implementação Erlang. O tempo incluiu a inicialização de toda a máquina virtual, executando seu programa e interrompendo a máquina virtual. Tenho certeza de que configurar e interromper o erlang vm leva algum tempo.

Se o tempo fosse feito dentro da própria máquina virtual erlang, os resultados seriam diferentes, pois nesse caso teríamos tempo real apenas para o programa em questão. Caso contrário, acredito que o tempo total gasto pelo processo de inicialização e carregamento do Erlang Vm mais o de interrompê-lo (como você o coloca no seu programa) estão incluídos no tempo total que o método que você está usando para cronometrar o programa está sendo emitido. Considere usar o próprio tempo erlang que usamos quando queremos cronometrar nossos programas na própria máquina virtual timer:tc/1 or timer:tc/2 or timer:tc/3. Dessa maneira, os resultados do erlang excluirão o tempo necessário para iniciar e parar / interromper / interromper a máquina virtual. Esse é o meu raciocínio, pense nisso e tente seu benchmark novamente.

Na verdade, sugiro que tentemos cronometrar o programa (para idiomas com tempo de execução), dentro do tempo de execução desses idiomas, a fim de obter um valor preciso. C, por exemplo, não tem sobrecarga de iniciar e desligar um sistema de tempo de execução, assim como Erlang, Python e Haskell (98% de certeza disso - eu estou corrigindo). Então (com base nesse raciocínio) concluo dizendo que esse benchmark não era preciso / justo o suficiente para idiomas rodando em cima de um sistema de tempo de execução. Vamos fazê-lo novamente com essas alterações.

EDIT: além de todas as linguagens possuírem sistemas de tempo de execução, a sobrecarga de iniciar cada um e interrompê-lo seria diferente. então sugiro que cronometramos nos sistemas de tempo de execução (para os idiomas aos quais isso se aplica). A Erlang VM é conhecida por ter uma sobrecarga considerável na inicialização!

Muzaaya Joshua
fonte
Esqueci de mencioná-lo no meu post, mas medi o tempo necessário para iniciar o sistema (cerca de 0,1 segundos na minha máquina). Isso é pequeno o suficiente em comparação com o tempo de execução do programa (cerca de 10 segundos) e não vale a pena discutir.
RichardC
na sua máquina! não sabemos se você está trabalhando em um servidor de incêndio solar !. Como o tempo é uma variável proporcional às especificações da máquina, isso deve ser levado em consideração ...
Muzaaya Joshua
2
@ RichardhardC Em nenhum lugar mencionado que Erlang é mais rápido :) Ele tem objetivos diferentes, não velocidade!
Exceção
7

Pergunta 1: Erlang, Python e Haskell perdem velocidade devido ao uso de números inteiros de comprimento arbitrário ou não, desde que os valores sejam menores que MAXINT?

A pergunta um pode ser respondida negativamente para Erlang. A última pergunta é respondida usando Erlang apropriadamente, como em:

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

Como é mais rápido que o seu exemplo inicial de C, eu acho que existem vários problemas, já que outros já abordaram em detalhes.

Este módulo Erlang é executado em um netbook barato em cerca de 5 segundos ... Ele usa o modelo de encadeamentos de rede em erlang e, como tal, demonstra como tirar proveito do modelo de evento. Pode ser distribuído por muitos nós. E é rápido. Não é o meu código.

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

O teste abaixo foi realizado em: CPU Intel (R) Atom (TM) N270 a 1,60GHz

~$ time erl -noshell -s p12dist start

The result is: 76576500.

^C

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a

real    0m5.510s
user    0m5.836s
sys 0m0.152s
Mark Washeim
fonte
aumentar o valor para 1000, como abaixo, não obtém o resultado correto. Com> 500 como acima, mais novo teste: IntelCore2 CPU 6600 @ 2.40GHz comletes em 0m2.370s reais
Mark Washeim
o seu resultado: 76576500 todos os outros: 842161320 ther é algo wronge com o seu resultado
davidDavidson
Como eu estava com alguns outros problemas de Euler, acabei de verificar meu resultado. A resposta para projecteuler.net/problem=12 é 76576500 sem dúvida. Eu sei que parece estranho, mas acabei de verificar.
Mark Washeim
Para comparação, recebo 9.03 com a versão c original. Ao usar o Erlang 19 com o código de Mark, recebo 5.406, 167.0366% mais rápido.
thanos
5

C ++ 11, <20ms para mim - Execute-o aqui

Entendo que você deseja dicas para ajudar a aprimorar seu conhecimento específico do idioma, mas como isso foi bem abordado aqui, pensei em adicionar um contexto para as pessoas que possam ter olhado o comentário do mathematica sobre sua pergunta, etc., e me perguntado por que isso código era muito mais lento.

Esta resposta é principalmente para fornecer contexto e, espero, ajudar as pessoas a avaliar o código em sua pergunta / outras respostas mais facilmente.

Esse código usa apenas algumas otimizações (feias), não relacionadas ao idioma usado, com base em:

  1. todo número de traingle tem a forma n (n + 1) / 2
  2. n e n + 1 são coprime
  3. o número de divisores é uma função multiplicativa

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

Isso leva em média 19ms no meu desktop e 80ms no meu laptop, muito longe da maioria dos outros códigos que eu já vi aqui. E há, sem dúvida, muitas otimizações ainda disponíveis.

user3125280
fonte
7
Isso não é explicitamente o que o solicitante solicitou: "Tentei realmente implementar o mesmo algoritmo o mais semelhante possível nas quatro línguas". Para citar um comentário em uma das muitas respostas excluídas semelhantes às suas "é bastante óbvio que você pode obter velocidades mais rápidas com um algoritmo melhor, independentemente do idioma".
Thomas M. DuBuisson
2
@ ThomasM.DuBuisson. É nisso que estou chegando. A pergunta \ respostas implica fortemente que as acelerações algorítmicas são significativas (e, é claro, o OP não está pedindo por elas), mas não há exemplo explícito. Eu acho que essa resposta - que não é exatamente um código altamente otimizado - fornece um contexto pouco útil para qualquer pessoa, como eu, que se perguntou o quão lento / rápido o código do OP era.
user3125280
O gcc pode até pré-calcular muitos padrões. int a = 0; para (int i = 0; i <10000000; ++ i) {a + = i;} será computado em tempo de compilação, portanto, tome <1ms em tempo de execução. Isso também vale #
Arthur
@ Thomas: Devo concordar com o user3125280 - as linguagens devem ser comparadas como eles se saem fazendo algo inteligente em vez de como não conseguem vencer uma linguagem de programação real ao fazer algo estúpido. Os algoritmos inteligentes geralmente se preocupam menos com a eficiência microscópica do que com a flexibilidade, a capacidade de conectar as coisas (combiná-las) e a infraestrutura. A questão não é tanto se um recebe 20 ms ou 50 ms, é não ficar 8 segundos ou 8 minutos.
DarthGizka
5

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

Eu recebo:

versão original c: 9.1690 100%
go: 8.2520 111%

Mas usando:

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
}

Eu recebo:

versão c original: 9.1690 100%
versão c do thaumkid: 0.1060 8650%
versão go primeiro: 8.2520 111%
versão go segundo: 0.0230 39865%

Eu também tentei Python3.6 e pypy3.3-5.5-alpha:

versão c original: 8.629 100%
versão c de thaumkid: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

e então com o seguinte código eu obtive:

versão c original: 8.629 100%
versão c do thaumkid: 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)
thanos
fonte
1

Mudança: case (divisor(T,round(math:sqrt(T))) > 500) of

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

Isso produzirá a resposta correta para o exemplo de multiprocessos Erlang.

user3051214
fonte
2
Isso foi planejado como um comentário para esta resposta ? Porque não está claro, e isso não é uma resposta por si só.
ShadowRanger 29/11
1

Eu assumi que o número de fatores só é grande se os números envolvidos tiverem muitos fatores pequenos. Então, usei o excelente algoritmo de thaumkid, mas primeiro usei uma aproximação à contagem de fatores que nunca é muito pequena. É bem simples: verifique se há fatores primos até 29, depois verifique o número restante e calcule um limite superior para o número de fatores. Use isso para calcular um limite superior para o número de fatores e, se esse número for alto o suficiente, calcule o número exato de fatores.

O código abaixo não precisa dessa suposição para correção, mas para ser rápido. Parece funcionar; apenas cerca de um em 100.000 números fornece uma estimativa alta o suficiente para exigir uma verificação completa.

Aqui está o código:

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

Isso encontra o 14.753.024º triangular com 13824 fatores em cerca de 0,7 segundos, o 879.207.615º número triangular com 61.440 fatores em 34 segundos, o 12.524.486.975º número triangular com 138.240 fatores em 10 minutos e 5 segundos e o 26.467.792.064º número triangular com 172.032 fatores em 21 minutos e 25 segundos (2,4 GHz Core2 Duo), portanto, esse código leva em média apenas 116 ciclos de processador por número. O último número triangular em si é maior que 2 ^ 68, portanto

gnasher729
fonte
0

Modifiquei a versão "Jannich Brendle" para 1000 em vez de 500. E liste o resultado de euler12.bin, euler12.erl, p12dist.erl. Ambos os códigos erl usam '+ nativo' para compilar.

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$
Witeman
fonte
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

time ./a.out

2.79s sistema 0.00s usuário 99% da CPU 2.794 total

user3250089
fonte