Comparación de velocidad con Project Euler: C vs Python vs Erlang vs Haskell

673

Tomé el Problema # 12 del Proyecto Euler como un ejercicio de programación y para comparar mis implementaciones (seguramente no óptimas) en C, Python, Erlang y Haskell. Para obtener tiempos de ejecución más altos, busco el primer número de triángulo con más de 1000 divisores en lugar de 500 como se indica en el problema original.

El resultado es el siguiente:

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ón:

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

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

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

Resumen:

  • C: 100%
  • Python: 692% (118% con PyPy)
  • Erlang: 436% (135% gracias a RichardC)
  • Haskell: 1421%

Supongo que C tiene una gran ventaja ya que usa largos para los cálculos y no enteros de longitud arbitraria como los otros tres. Además, no necesita cargar un tiempo de ejecución primero (¿los otros?).

Pregunta 1: ¿Erlang, Python y Haskell pierden velocidad debido al uso de enteros de longitud arbitraria o no, siempre que los valores sean menores MAXINT?

Pregunta 2: ¿Por qué Haskell es tan lento? ¿Hay una bandera del compilador que apaga los frenos o es mi implementación? (Esto último es bastante probable ya que Haskell es un libro con siete sellos para mí).

Pregunta 3: ¿Puede ofrecerme algunas sugerencias sobre cómo optimizar estas implementaciones sin cambiar la forma en que determino los factores? Optimización de cualquier forma: más agradable, más rápida, más "nativa" del idioma.

EDITAR:

Pregunta 4: ¿Mis implementaciones funcionales permiten LCO (optimización de última llamada, también conocida como eliminación de recursión de cola) y, por lo tanto, evitan agregar marcos innecesarios en la pila de llamadas?

Realmente intenté implementar el mismo algoritmo lo más similar posible en los cuatro idiomas, aunque tengo que admitir que mi conocimiento de Haskell y Erlang es muy limitado.


Códigos fuente utilizados:

#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
Hiperbóreo
fuente
55
@Jochen (y Seth) No es realmente que C sea rápido o asombroso, pero se percibe como fácil de escribir código de rendimiento (puede que no sea cierto, pero la mayoría de los programas parecen ser capaces, lo suficientemente cierto). A medida que exploro en mi respuesta, y he descubierto que es cierto con el tiempo, la habilidad del programador y el conocimiento de optimizaciones comunes para el idioma elegido es de gran importancia (especialmente para Haskell).
Thomas M. DuBuisson
52
Acaba de comprobar con Mathematica - que se necesita 0.25sec (con C que se necesita 6sec aquí), y el código es simplemente: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. ¡Hurra!
tsvikas
35
¿Hay alguien más que recuerde estas guerras entre C y la asamblea? "¡Claro! Puedes escribir tu código 10 veces más rápido en C, pero ¿puede tu código C correr tan rápido? ..." Estoy seguro de que se libraron las mismas batallas entre el código de máquina y el ensamblaje.
JS.
39
@JS: Probablemente no, ya que el ensamblaje es simplemente un conjunto de mnemotécnicos que escribes en lugar del código de máquina binario sin formato; normalmente, existe una correspondencia de 1-1 entre ellos.
Callum Rogers
99
la conclusión, para Haskell: -O2 le da una aceleración de aproximadamente 3x, y usando Int en lugar de Integer aproximadamente 4x-6x para la aceleración total de 12x-14x y más.
Will Ness

Respuestas:

794

Usando GHC 7.0.3, gcc 4.4.6, Linux 2.6.29en una Core2 Duo máquina x86_64 (2,5 GHz), la compilación usando ghc -O2 -fllvm -fforce-recomppara Haskell y gcc -O3 -lmpara C.

  • Su rutina C se ejecuta en 8.4 segundos (probablemente más rápido que su carrera debido a -O3)
  • La solución de Haskell se ejecuta en 36 segundos (debido a la -O2bandera)
  • Su factorCount'código no está escrito de manera explícita y por defecto Integer(¡gracias a Daniel por corregir mi diagnóstico erróneo aquí!). Dar una firma de tipo explícito (que es la práctica estándar de todos modos) usando Inty el tiempo cambia a 11.1 segundos
  • en factorCount'ti has llamado innecesariamente fromIntegral. Sin embargo, una solución no produce cambios (el compilador es inteligente, por suerte para usted)
  • Usaste moddonde remes más rápido y suficiente. Esto cambia el tiempo a 8,5 segundos .
  • factorCount'aplica constantemente dos argumentos adicionales que nunca cambian ( number, sqrt). Una transformación trabajador / envoltorio nos da:
 $ time ./so
 842161320  

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

Así es, 7.95 segundos . Consistentemente medio segundo más rápido que la solución C . Sin la -fllvmbandera que sigo recibiendo 8.182 seconds, por lo que el backend de NCG también funciona bien en este caso.

Conclusión: Haskell es asombroso.

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

EDITAR: ahora que hemos explorado eso, abordemos las preguntas

Pregunta 1: ¿Erlang, Python y Haskell pierden velocidad debido al uso de enteros de longitud arbitraria o no, siempre que los valores sean inferiores a MAXINT?

En Haskell, el uso Integeres más lento que, Intpero cuánto más lento depende de los cálculos realizados. Afortunadamente (para máquinas de 64 bits) Intes suficiente. Por razones de portabilidad, probablemente debería volver a escribir mi código para usar Int64o Word64(C no es el único lenguaje con a long).

Pregunta 2: ¿Por qué Haskell es tan lento? ¿Hay una bandera del compilador que apaga los frenos o es mi implementación? (Esto último es bastante probable ya que Haskell es un libro con siete sellos para mí).

Pregunta 3: ¿Puede ofrecerme algunas sugerencias sobre cómo optimizar estas implementaciones sin cambiar la forma en que determino los factores? Optimización de cualquier forma: más agradable, más rápida, más "nativa" del idioma.

Eso fue lo que respondí arriba. La respuesta fue

  • 0) Utilice la optimización a través de -O2
  • 1) Use tipos rápidos (notablemente: sin caja) cuando sea posible
  • 2) remno mod(una optimización frecuentemente olvidada) y
  • 3) transformación trabajador / contenedor (quizás la optimización más común).

Pregunta 4: ¿Mis implementaciones funcionales permiten LCO y, por lo tanto, evitan agregar marcos innecesarios a la pila de llamadas?

Sí, ese no era el problema. Buen trabajo y me alegro de que hayas considerado esto.

Thomas M. DuBuisson
fuente
25
@Karl Porque remes en realidad un subcomponente de la modoperación (no son lo mismo). Si busca en la biblioteca Base de GHC, verá modpruebas para varias condiciones y ajusta el signo en consecuencia. (ver modInt#en Base.lhs)
Thomas M. DuBuisson
20
Otro punto de datos: escribí una traducción rápida de Haskell del programa C sin mirar Haskell de @ Hyperboreus. Así que es un poco más cerca de Haskell idiomática estándar, y la única optimización he añadido deliberadamente está reemplazando modcon remdespués de leer esta respuesta (je, vaya). Vea el enlace para mis horarios, pero la versión corta es "casi idéntica a la C".
CA McCann
106
Aunque pensé que la versión C funcionaba más rápido en mi máquina, ahora tengo un nuevo respeto por Haskell. +1
Seth Carnegie
11
Esto es bastante sorprendente para mí, aunque aún no lo he probado. Dado que el original factorCount'era recursivo de cola, habría pensado que el compilador podría detectar los parámetros adicionales que no se cambian y optimizar la recursión de cola solo para los parámetros cambiantes (después de todo, Haskell es un lenguaje puro, esto debería ser fácil). ¿Alguien piensa que el compilador podría hacer eso o debería volver a leer más documentos de teoría?
kizzx2
22
@ kizzx2: hay un ticket de GHC para agregarlo. Por lo que he entendido, esta transformación puede resultar en asignaciones adicionales de objetos de cierre. Esto significa un peor rendimiento en algunos casos, pero como sugiere Johan Tibell en su publicación de blog, esto se puede evitar si el contenedor resultante se puede insertar.
hammar
224

Hay algunos problemas con la implementación de Erlang. Como referencia para lo siguiente, mi tiempo de ejecución medido para su programa Erlang no modificado fue de 47.6 segundos, en comparación con 12.7 segundos para el código C.

Lo primero que debe hacer si desea ejecutar código Erlang computacionalmente intensivo es usar código nativo. Compilar con erlc +native euler12el tiempo se redujo a 41,3 segundos. Sin embargo, esta es una aceleración mucho menor (solo 15%) de lo esperado de la compilación nativa en este tipo de código, y el problema es su uso -compile(export_all). Esto es útil para la experimentación, pero el hecho de que todas las funciones sean potencialmente accesibles desde el exterior hace que el compilador nativo sea muy conservador. (El emulador BEAM normal no se ve tan afectado). Reemplazar esta declaración por -export([solve/0]).una aceleración mucho mejor: 31.5 segundos (casi 35% desde la línea de base).

Pero el código en sí tiene un problema: para cada iteración en el bucle factorCount, realiza esta prueba:

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

El código C no hace esto. En general, puede ser complicado hacer una comparación equitativa entre diferentes implementaciones del mismo código, y en particular si el algoritmo es numérico, porque debe asegurarse de que realmente estén haciendo lo mismo. Un ligero error de redondeo en una implementación debido a algún tipo de conversión en alguna parte puede hacer que realice muchas más iteraciones que la otra, aunque ambas finalmente alcanzan el mismo resultado.

Para eliminar esta posible fuente de error (y deshacerme de la prueba adicional en cada iteración), reescribí la función factorCount de la siguiente manera, estrechamente modelada en el 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.

Esta export_allcompilación de reescritura, no y nativa, me dio el siguiente tiempo de ejecución:

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

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

que no es tan malo en comparación con el código C:

$ time ./a.out 
842161320

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

teniendo en cuenta que Erlang no está orientado en absoluto a escribir código numérico, es solo un 50% más lento que C en un programa como este es bastante bueno.

Finalmente, con respecto a sus preguntas:

Pregunta 1: ¿Erlang, Python y Haskell pierden velocidad debido al uso de enteros de longitud arbitraria o no, siempre que los valores sean inferiores a MAXINT?

Si un poco. En Erlang, no hay forma de decir "usar aritmética de 32/64 bits con ajuste", así que a menos que el compilador pueda probar algunos límites en sus enteros (y generalmente no puede), debe verificar todos los cálculos para ver si pueden caber en una sola palabra etiquetada o si tiene que convertirlos en bignums asignados en el montón. Incluso si nunca se usan bignums en la práctica en tiempo de ejecución, estas comprobaciones deberán realizarse. Por otro lado, eso significa que sabe que el algoritmo nunca fallará debido a una envoltura entera inesperada si de repente le da entradas más grandes que antes.

Pregunta 4: ¿Mis implementaciones funcionales permiten LCO y, por lo tanto, evitan agregar marcos innecesarios a la pila de llamadas?

Sí, su código Erlang es correcto con respecto a la optimización de la última llamada.

RichardC
fuente
2
Estoy de acuerdo contigo. Este punto de referencia no fue preciso, especialmente para Erlang por varias razones
Muzaaya Joshua el
156

En lo que respecta a la optimización de Python, además de usar PyPy (para aceleraciones bastante impresionantes con cero cambios en su código), puede usar la cadena de herramientas de traducción de PyPy para compilar una versión compatible con RPython, o Cython para construir un módulo de extensión, ambos que son más rápidos que la versión C en mis pruebas, con el módulo Cython casi el doble de rápido . Como referencia también incluyo los resultados de referencia de C y PyPy:

C (compilado con 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 la última revisión de 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

La versión RPython tiene un par de cambios clave. Para traducir a un programa independiente, necesita definir su target, que en este caso es la mainfunción. Se espera que acepte sys.argvcomo único argumento, y se requiere que devuelva un int. Puede traducirlo utilizando translate.py, % translate.py euler12-rpython.pyque se traduce a C y lo compila por usted.

# 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

La versión de Cython fue reescrita como un módulo de extensión _euler12.pyx, que importo y llamo desde un archivo normal de Python. El _euler12.pyxes esencialmente la misma que la versión, con algunas declaraciones de tipo estáticas adicionales. El setup.py tiene la plantilla normal para construir la extensión, 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, tengo muy poca experiencia con RPython o Cython, y me sorprendieron gratamente los resultados. Si está utilizando CPython, escribir sus bits de código intensivos en CPU en un módulo de extensión de Cython parece una forma realmente fácil de optimizar su programa.

zeekay
fuente
66
Tengo curiosidad, ¿se puede optimizar la versión C para que sea al menos tan rápida como CPython?
Nombre para mostrar el
44
@SargeBorsch esa versión de Cython es tan rápida, ya que se compila en una fuente C altamente optimizada, lo que significa que seguramente puede obtener ese rendimiento de C.
Eli Korvigo
72

Pregunta 3: ¿Puede ofrecerme algunas sugerencias sobre cómo optimizar estas implementaciones sin cambiar la forma en que determino los factores? Optimización de cualquier forma: más agradable, más rápida, más "nativa" del idioma.

La implementación de C es subóptima (como lo insinuó Thomas M. DuBuisson), la versión usa enteros de 64 bits (es decir, tipo de datos largo ). Investigaré la lista de ensamblaje más tarde, pero con una suposición educada, hay algunos accesos a la memoria en el código compilado, lo que hace que el uso de enteros de 64 bits sea significativamente más lento. Es eso o el código generado (ya sea el hecho de que puede colocar menos entradas de 64 bits en un registro SSE o redondear un número entero de doble a 64 bits es más lento).

Aquí está el código modificado (simplemente reemplace long con int y explícitamente incluí factorCount, aunque no creo que sea necesario con 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 + sincronización da:

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

Como referencia, la implementación de haskell por Thomas en la respuesta anterior da:

$ 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

Conclusión: sin quitarle nada a ghc, es un gran compilador, pero gcc normalmente genera un código más rápido.

Raedwulf
fuente
22
¡Muy agradable! A modo de comparación, en mi máquina, su solución C se ejecuta 2.5 secondsmientras que una modificación similar al código Haskell (pasar a Word32, agregar pragma INLINE) da como resultado un tiempo de ejecución de 4.8 seconds. Tal vez se pueda hacer algo (al parecer, no de manera trivial): el resultado de gcc es ciertamente impresionante.
Thomas M. DuBuisson
1
¡Gracias! Quizás la pregunta debería ser la velocidad de salida compilada por varios compiladores en lugar del lenguaje real en sí. Por otra parte, sacar los manuales de Intel y optimizar a mano seguirá ganando directamente (siempre que tenga el conocimiento y el tiempo (mucho)).
Raedwulf
56

Echa un vistazo a este blog . Durante el año pasado, más o menos, ha solucionado algunos de los problemas del Proyecto Euler en Haskell y Python, y generalmente ha descubierto que Haskell es mucho más rápido. Creo que entre esos idiomas tiene más que ver con tu fluidez y estilo de codificación.

Cuando se trata de la velocidad de Python, ¡estás usando una implementación incorrecta! Pruebe PyPy , y para cosas como esta, encontrará que es mucho, mucho más rápido.

agf
fuente
32

Su implementación de Haskell podría acelerarse enormemente utilizando algunas funciones de los paquetes de Haskell. En este caso usé primos, que se acaba de instalar con '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)

Tiempos:

Tu programa original:

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

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Implementación mejorada

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

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Como puede ver, este se ejecuta en 38 milisegundos en la misma máquina donde la suya se ejecutó en 16 segundos :)

Comandos de compilación:

ghc -O2 012.hs -o bin\012.exe
ghc -O2 012_slow.hs -o bin\012_slow.exe
Connie Hilarides
fuente
55
La última vez que verifiqué los "primos" de Haskell fue solo una gran lista de primos precalculados, sin cálculo, solo búsqueda. Entonces sí, por supuesto, esto será más rápido, pero no le dice nada sobre la velocidad computacional de derivar primos en Haskell.
zxq9
21
@ zxq9 ¿podría indicarme en qué parte de la fuente del paquete de números primos ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ) se encuentra la lista de números primos?
Fraser
44
Si bien la fuente muestra que los números primos no están calculados previamente, esta aceleración es completamente loca, millas más rápido que la versión C, entonces, ¿qué diablos está pasando?
punto
1
@semicolon memorización. En este caso, creo que Haskell memorizó todos los primos en tiempo de ejecución, por lo que no es necesario volver a calcularlos en cada iteración.
Hauleth
55
Son 1000 divisores, no 500.
Casper Færgemand
29

Solo por diversión. La siguiente es una implementación de Haskell más '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

Utilizando ghc -O3, esto se ejecuta constantemente en 0.55-0.58 segundos en mi máquina (1.73GHz Core i7).

Una función factorCount más eficiente para la versión C:

int factorCount (int n)
{
  int count = 1;
  int candidate,tmpCount;
  while (n % 2 == 0) {
    count++;
    n /= 2;
  }
    for (candidate = 3; candidate < n && candidate * candidate < n; candidate += 2)
    if (n % candidate == 0) {
      tmpCount = 1;
      do {
        tmpCount++;
        n /= candidate;
      } while (n % candidate == 0);
       count*=tmpCount;
      }
  if (n > 1)
    count *= 2;
  return count;
}

Cambiando longs a ints en main, usando gcc -O3 -lm, esto se ejecuta consistentemente en 0.31-0.35 segundos.

Se puede hacer que ambos funcionen aún más rápido si aprovecha el hecho de que el enésimo número de triángulo = n * (n + 1) / 2, yn y (n + 1) tienen factorizaciones primas completamente dispares, por lo que el número de factores de cada mitad se puede multiplicar para encontrar el número de factores del todo. El seguimiento:

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

reducirá el tiempo de ejecución del código c a 0.17-0.19 segundos, y puede manejar búsquedas mucho más grandes: más de 10000 factores toman alrededor de 43 segundos en mi máquina. Dejo una aceleración de haskell similar para el lector interesado.

revs thaumkid
fuente
3
Solo para comparación: versión original c: 9.1690, versión de thaumkid: 0.1060 86x mejora.
gracias
Guau. Haskell funciona muy bien una vez que evitas los tipos inferidos
Piyush Katariya
En realidad no es la inferencia que lo hizo. Eso solo le ayuda a A) depurar o evitar problemas de tipo y problemas de selección de instancia de tipo de clase B) depurar y evitar algunos problemas de tipo indecidibles con algunas extensiones de lenguaje modernas. También le ayuda a hacer que sus programas sean invencibles para que nunca pueda ampliar sus esfuerzos de desarrollo.
código de
c versión 0.11 s en Intel skull canyon
codehot
13
Pregunta 1: ¿Erlang, Python y Haskell pierden velocidad debido al uso de enteros de longitud arbitraria o no, siempre que los valores sean inferiores a MAXINT?

Esto es poco probable No puedo decir mucho sobre Erlang y Haskell (bueno, tal vez un poco sobre Haskell a continuación) pero puedo señalar muchos otros cuellos de botella en Python. Cada vez que el programa intenta ejecutar una operación con algunos valores en Python, debe verificar si los valores son del tipo adecuado y cuesta un poco de tiempo. Su factorCountfunción solo asigna una lista con range (1, isquare + 1)varias veces, y mallocla asignación de memoria con estilo de tiempo de ejecución es mucho más lenta que iterar en un rango con un contador como lo hace en C. Notablemente, factorCount()se llama varias veces y, por lo tanto, asigna muchas listas. Además, no olvidemos que Python se interpreta y que el intérprete de CPython no tiene un gran enfoque en la optimización.

EDITAR : oh, bueno, noto que está utilizando Python 3, por lo range()que no devuelve una lista, sino un generador. En este caso, mi punto sobre la asignación de listas es medio erróneo: la función solo asigna rangeobjetos, que no obstante son ineficientes pero no tan ineficientes como asignar una lista con muchos elementos.

Pregunta 2: ¿Por qué Haskell es tan lento? ¿Hay una bandera del compilador que apaga los frenos o es mi implementación? (Esto último es bastante probable ya que Haskell es un libro con siete sellos para mí).

¿Estás usando abrazos ? Hugs es un intérprete considerablemente lento. Si lo está utilizando, tal vez pueda obtener un mejor momento con GHC , pero solo estoy reflexionando sobre la hipotesis, el tipo de cosas que un buen compilador de Haskell hace bajo el capó es bastante fascinante y está más allá de mi comprensión :)

Pregunta 3: ¿Puede ofrecerme algunas sugerencias sobre cómo optimizar estas implementaciones sin cambiar la forma en que determino los factores? Optimización de cualquier forma: más agradable, más rápida, más "nativa" del idioma.

Yo diría que estás jugando un juego divertido. La mejor parte de conocer varios idiomas es usarlos de la manera más diferente posible :) Pero estoy divagando, simplemente no tengo ninguna recomendación para este punto. Lo siento, espero que alguien pueda ayudarte en este caso :)

Pregunta 4: ¿Mis implementaciones funcionales permiten LCO y, por lo tanto, evitan agregar marcos innecesarios a la pila de llamadas?

Hasta donde recuerdo, solo necesita asegurarse de que su llamada recursiva sea el último comando antes de devolver un valor. En otras palabras, una función como la siguiente podría usar dicha optimización:

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

Sin embargo, no tendría tal optimización si su función fuera como la siguiente, porque hay una operación (multiplicación) después de la llamada recursiva:

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

Separé las operaciones en algunas variables locales para dejar en claro qué operaciones se ejecutan. Sin embargo, lo más habitual es ver estas funciones a continuación, pero son equivalentes para el punto que estoy haciendo:

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

Tenga en cuenta que depende del compilador / intérprete decidir si hará una recursión de cola. Por ejemplo, el intérprete de Python no lo hace si recuerdo bien (usé Python en mi ejemplo solo por su sintaxis fluida). De todos modos, si encuentra cosas extrañas como funciones factoriales con dos parámetros (y uno de los parámetros tiene nombres como acc, accumulatoretc.) ahora sabe por qué la gente lo hace :)

brandizzi
fuente
@Hyperboreus gracias! Además, tengo mucha curiosidad sobre sus próximas preguntas. Sin embargo, le advierto que mi conocimiento es limitado, por lo que no podría responder a todas sus preguntas. Para tratar de compensarlo, hice mi wiki de comunidad de respuestas para que las personas puedan complementarlo más fácilmente.
brandizzi
Sobre el uso del rango. Cuando reemplazo el rango con un ciclo while con incremento (imitando el ciclo for de C), el tiempo de ejecución en realidad se duplica. Supongo que los generadores están bastante optimizados.
Hyperboreus
12

Con Haskell, realmente no necesita pensar explícitamente en las recursiones.

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

En el código anterior, he reemplazado las recurrencias explícitas en la respuesta de @Thomas con operaciones de lista comunes. El código sigue haciendo exactamente lo mismo sin que nos preocupemos por la recursividad de la cola. Se ejecuta (~ 7.49s ) aproximadamente un 6% más lento que la versión en la respuesta de @Thomas (~ 7.04s ) en mi máquina con GHC 7.6.2, mientras que la versión C de @Raedwulf funciona con ~ 3.15s . Parece que GHC ha mejorado durante el año.

PD. Sé que es una vieja pregunta, y me topé con ella en las búsquedas de Google (olvidé lo que estaba buscando, ahora ...). Solo quería comentar la pregunta sobre LCO y expresar mis sentimientos sobre Haskell en general. Quería comentar sobre la respuesta principal, pero los comentarios no permiten bloques de código.

jxy
fuente
9

Algunos números más y explicaciones para la versión C. Al parecer, nadie lo hizo durante todos esos años. Recuerde votar esta respuesta para que todos puedan ver y aprender.

Paso uno: Benchmark de los programas del autor.

Especificaciones de la computadora portátil:

  • CPU i3 M380 (931 MHz - modo de ahorro máximo de batería)
  • 4GB de memoria
  • Win7 64 bits
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin con 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

Los nombres de archivo son: integertype_architecture_compiler.exe

  • integertype es el mismo que el programa original por ahora (más sobre eso más adelante)
  • la arquitectura es x86 o x64 dependiendo de la configuración del compilador
  • el compilador es gcc o vs2012

Paso dos: investigue, mejore y vuelva a comparar

VS es 250% más rápido que gcc. Los dos compiladores deberían dar una velocidad similar. Obviamente, algo está mal con el código o las opciones del compilador. ¡A investigar!

El primer punto de interés son los tipos enteros. Las conversiones pueden ser costosas y la consistencia es importante para una mejor generación de código y optimizaciones. Todos los enteros deben ser del mismo tipo.

Es un desastre mixto inty en longeste momento. Vamos a mejorar eso. ¿Qué tipo usar? El más rápido. ¡Tengo que compararlos a todos!

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

Los tipos enteros son int long int32_t uint32_t int64_ty uint64_tde#include <stdint.h>

Hay MUCHOS tipos de enteros en C, más algunos con y sin signo para jugar, más la opción de compilar como x86 o x64 (no confundir con el tamaño entero real). Esas son muchas versiones para compilar y ejecutar ^^

Paso tres: comprender los números

Conclusiones definitivas:

  • Los enteros de 32 bits son ~ 200% más rápidos que los equivalentes de 64 bits
  • los enteros de 64 bits sin signo son un 25% más rápidos que los 64 bits con signo (Lamentablemente, no tengo ninguna explicación para eso)

Pregunta capciosa: "¿Cuáles son los tamaños de int y long en C?"
La respuesta correcta es: ¡ El tamaño de int y long en C no está bien definido!

De la especificación C:

int tiene al menos 32 bits de
longitud es al menos un int

Desde la página de manual de gcc (banderas -m32 y -m64):

El entorno de 32 bits establece int, long y puntero en 32 bits y genera código que se ejecuta en cualquier sistema i386.
El entorno de 64 bits establece int a 32 bits y largo y puntero a 64 bits y genera código para la arquitectura x86-64 de AMD.

De la documentación de MSDN (rangos de tipo de datos) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 bytes, también se conoce como
largo firmado , 4 bytes, también se conoce como largo int y largo int firmado

Para concluir esto: lecciones aprendidas

  • Los enteros de 32 bits son más rápidos que los enteros de 64 bits.

  • Los tipos enteros estándar no están bien definidos en C ni C ++, varían según los compiladores y las arquitecturas. Cuando necesite consistencia y previsibilidad, use la uint32_tfamilia entera de #include <stdint.h>.

  • Problemas de velocidad resueltos. Todos los demás lenguajes han retrocedido cientos por ciento, ¡C & C ++ vuelve a ganar! Ellos siempre lo hacen. La próxima mejora será multihilo con OpenMP: D

usuario5994461
fuente
Por curiosidad, ¿cómo funcionan los compiladores Intel? Por lo general, son muy buenos para optimizar el código numérico.
kirbyfan64sos
¿Dónde encuentra una referencia que diga que la especificación C garantiza que "int es al menos 32 bits"? Las únicas garantías que conozco son las magnitudes mínimas de INT_MINy INT_MAX(-32767 y 32767, que prácticamente imponen un requisito de intal menos 16 bits). longse requiere que sea al menos tan grande como an int, y el promedio de requisitos de rango longes de al menos 32 bits.
ShadowRanger
Pareces estar en lo correcto. stackoverflow.com/questions/1231147/is-int-in-c-always-32-bit
user5994461
8

Mirando su implementación de Erlang. El tiempo ha incluido el inicio de toda la máquina virtual, ejecutar su programa y detener la máquina virtual. Estoy bastante seguro de que configurar y detener el erlang vm lleva algún tiempo.

Si el tiempo se realizó dentro de la máquina virtual de erlang, los resultados serían diferentes ya que en ese caso tendríamos el tiempo real solo para el programa en cuestión. De lo contrario, creo que el tiempo total que lleva el proceso de inicio y carga de Erlang Vm más el de detenerlo (como lo incluyó en su programa) se incluyen en el tiempo total que utiliza el método para cronometrar El programa está saliendo. Considere usar el tiempo de erlang mismo que usamos cuando queremos cronometrar nuestros programas dentro de la máquina virtual timer:tc/1 or timer:tc/2 or timer:tc/3. De esta manera, los resultados de erlang excluirán el tiempo necesario para iniciar y detener / matar / detener la máquina virtual. Ese es mi razonamiento allí, piénselo y luego intente su punto de referencia nuevamente.

De hecho, sugiero que intentemos cronometrar el programa (para los idiomas que tienen un tiempo de ejecución), dentro del tiempo de ejecución de esos idiomas para obtener un valor preciso. C, por ejemplo, no tiene sobrecarga de iniciar y apagar un sistema de tiempo de ejecución al igual que Erlang, Python y Haskell (98% seguro de esto, estoy de acuerdo con la corrección). Entonces (basado en este razonamiento) concluyo diciendo que este punto de referencia no era lo suficientemente preciso / justo para los idiomas que se ejecutan sobre un sistema de tiempo de ejecución. Hagámoslo nuevamente con estos cambios.

EDITAR: además, incluso si todos los idiomas tuvieran sistemas de tiempo de ejecución, la sobrecarga de iniciar cada uno y detenerlo sería diferente. así que sugiero que tomemos el tiempo desde los sistemas de tiempo de ejecución (para los idiomas para los que esto se aplica). ¡Se sabe que Erlang VM tiene una sobrecarga considerable en el arranque!

Muzaaya Joshua
fuente
Olvidé mencionarlo en mi publicación, pero medí el tiempo que toma solo para iniciar el sistema (erl -noshell -s erlang halt), alrededor de 0.1 segundos en mi máquina. Esto es lo suficientemente pequeño en comparación con el tiempo de ejecución del programa (alrededor de 10 segundos) que no vale la pena discutir.
RichardC
en tu máquina! ¡No sabemos si está trabajando en un servidor Sun Fire! Dado que el tiempo es una variable proporcional a las especificaciones de la máquina, debe tenerse en cuenta ... ¿discutiendo?
Muzaaya Joshua
2
@RichardC Nowhere mencionó que Erlang es más rápido :) ¡Tiene diferentes objetivos, no velocidad!
Excepción el
7

Pregunta 1: ¿Erlang, Python y Haskell pierden velocidad debido al uso de enteros de longitud arbitraria o no, siempre que los valores sean inferiores a MAXINT?

La pregunta uno se puede responder negativamente para Erlang. La última pregunta se responde usando Erlang adecuadamente, como en:

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

Como es más rápido que su ejemplo inicial de C, supongo que hay numerosos problemas, ya que otros ya lo han cubierto en detalle.

Este módulo Erlang se ejecuta en una netbook barata en aproximadamente 5 segundos ... Utiliza el modelo de hilos de red en erlang y, como tal, muestra cómo aprovechar el modelo de eventos. Se podría distribuir en muchos nodos. Y es rapido. No es mi codigo.

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

La prueba a continuación se realizó en una: Intel (R) Atom (TM) CPU N270 @ 1.60GHz

~$ time erl -noshell -s p12dist start

The result is: 76576500.

^C

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

real    0m5.510s
user    0m5.836s
sys 0m0.152s
Mark Washeim
fuente
aumentar el valor a 1000 como se muestra a continuación no obtiene el resultado correcto. Con> 500 como el anterior, la prueba más reciente: IntelCore2 CPU 6600 @ 2.40GHz compite en 0m2.370s reales
Mark Washeim
el resultado: 76576500 todos los demás: 842161320 allí es algo wronge con su resultado
davidDavidson
Como tenía otros problemas de Euler, simplemente verifiqué mi resultado. La respuesta a projecteuler.net/problem=12 es 76576500, no hay duda al respecto. Sé que parece extraño, pero acabo de comprobarlo.
Mark Washeim
A modo de comparación, obtengo 9.03 con la versión original de c mientras uso Erlang 19 con el código de Mark, obtengo 5.406, 167.0366% más rápido.
gracias
5

C ++ 11, <20 ms para mí : ejecútelo aquí

Entiendo que desea consejos para ayudar a mejorar su conocimiento específico del idioma, pero dado que eso se ha cubierto bien aquí, pensé que agregaría un contexto para las personas que pudieron haber visto el comentario matemático sobre su pregunta, etc., y me pregunté por qué esto El código fue mucho más lento.

Esta respuesta es principalmente para proporcionar contexto para ayudar a las personas a evaluar el código en su pregunta / otras respuestas más fácilmente.

Este código usa solo un par de optimizaciones (feas), no relacionadas con el lenguaje utilizado, basado en:

  1. cada número de traingle tiene la forma n (n + 1) / 2
  2. n y n + 1 son coprimos
  3. el número de divisores es una función 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;
}

Eso toma alrededor de 19 ms en promedio para mi escritorio y 80 ms para mi computadora portátil, muy lejos de la mayoría del otro código que he visto aquí. Y hay, sin duda, muchas optimizaciones aún disponibles.

usuario3125280
fuente
77
Esto no es explícitamente lo que solicitó el autor de la pregunta: "Realmente intenté implementar el mismo algoritmo lo más similar posible en los cuatro idiomas". Para citar un comentario sobre una de las muchas respuestas eliminadas similares a las suyas "es bastante obvio que puede obtener velocidades más rápidas con un mejor algoritmo independientemente del idioma".
Thomas M. DuBuisson
2
@ ThomasM.DuBuisson. A eso me refiero. La pregunta \ las respuestas implican en gran medida que las aceleraciones algorítmicas son significativas (y, por supuesto, OP no las solicita), pero no hay un ejemplo explícito. Creo que esta respuesta, que no es exactamente un código altamente optimizado, proporciona un contexto poco útil para cualquier persona, como yo, que se pregunta qué tan lento / rápido es el código del OP.
user3125280
gcc incluso puede precalcular muchos patrones. int a = 0; for (int i = 0; i <10000000; ++ i) {a + = i;} se calculará en tiempo de compilación, por lo tanto, tome <1 ms en tiempo de ejecución. También cuenta
Arthur
@Thomas: Debo estar de acuerdo con user3125280: los idiomas deben compararse con la forma en que hacen algo inteligente en lugar de cómo no logran vencer a un lenguaje de programación real al hacer algo tonto. Los algoritmos inteligentes generalmente se preocupan menos por las eficiencias microscópicas que por la flexibilidad, la capacidad de conectar cosas (combinarlas) y la infraestructura. La cuestión no es tanto si uno tiene 20 ms o 50 ms, se no consiguiendo 8 segundos o 8 minutos.
DarthGizka
5

Intentando IR:

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

Yo obtengo:

versión original c: 9.1690 100%
ir: 8.2520 111%

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

Yo obtengo:

versión c original: 9.1690 100%
versión c de thaumkid: 0.1060 8650%
primera versión de versión: 8.2520 111%
segunda versión de versión: 0.0230 39865%

También probé Python3.6 y pypy3.3-5.5-alpha:

versión c original: 8.629 100%
versión c de thaumkid: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

y luego con el siguiente código obtuve:

versión c original: 8.629 100%
versión c de 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)
gracias
fuente
1

Cambio: case (divisor(T,round(math:sqrt(T))) > 500) of

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

Esto producirá la respuesta correcta para el ejemplo multiproceso de Erlang.

usuario3051214
fuente
2
¿Fue esto como un comentario sobre esta respuesta ? Porque no está claro, y esta no es una respuesta en sí misma.
ShadowRanger
1

Supuse que el número de factores solo es grande si los números involucrados tienen muchos factores pequeños. Así que utilicé el excelente algoritmo de thaumkid, pero primero usé una aproximación al conteo de factores que nunca es demasiado pequeño. Es bastante simple: verifique los factores primos hasta 29, luego verifique el número restante y calcule un límite superior para el número de factores. Use esto para calcular un límite superior para el número de factores, y si ese número es lo suficientemente alto, calcule el número exacto de factores.

El siguiente código no necesita esta suposición para la corrección, sino para ser rápido. Parece funcionar; solo alrededor de uno de cada 100,000 números da una estimación lo suficientemente alta como para requerir una verificación completa.

Aquí está el 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;
            }
        }
    }
}

Esto encuentra el número triangular 14,753,024 con 13824 factores en aproximadamente 0,7 segundos, el número triangular 879,207,615 con 61,440 factores en 34 segundos, el número triangular 12,524,486,975 con factores 138,240 en 10 minutos y 5 segundos, y el número triangular 26,467,792,064 con 172,032 factores en 21 minutos 25 segundos (Core2 Duo a 2,4 GHz), por lo que este código solo toma 116 ciclos de procesador por número en promedio. El último número triangular en sí es mayor que 2 ^ 68, por lo que

gnasher729
fuente
0

Modifiqué la versión de "Jannich Brendle" a 1000 en lugar de 500. Y enumere el resultado de euler12.bin, euler12.erl, p12dist.erl. Ambos códigos erl usan '+ native' 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
fuente
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

tiempo ./a.out

2.79s usuario 0.00s sistema 99% cpu 2.794 total

usuario3250089
fuente