Confronto di velocità con Project Euler: C vs Python vs Erlang vs Haskell


672

Ho preso il Problema n. 12 da Project Euler come esercizio di programmazione e per confrontare le mie (sicuramente non ottimali) implementazioni in C, Python, Erlang e Haskell. Per ottenere tempi di esecuzione più alti, cerco il primo numero di triangolo con più di 1000 divisori anziché 500 come indicato nel problema originale.

Il risultato è il seguente:

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

Pitone:

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

Sommario:

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

Suppongo che C abbia un grande vantaggio in quanto utilizza long per i calcoli e numeri interi non arbitrari come gli altri tre. Inoltre, non è necessario caricare prima un runtime (fare gli altri?).

Domanda 1: Erlang, Python e Haskell perdono velocità a causa dell'uso di numeri interi di lunghezza arbitraria o non finché i valori sono inferiori a MAXINT?

Domanda 2: Perché Haskell è così lento? C'è una bandiera del compilatore che spegne i freni o è la mia implementazione? (Quest'ultimo è abbastanza probabile poiché Haskell è un libro con sette sigilli per me.)

Domanda 3: Potete offrirmi alcuni suggerimenti su come ottimizzare queste implementazioni senza cambiare il modo in cui determino i fattori? Ottimizzazione in qualsiasi modo: più bella, più veloce, più "nativa" della lingua.

MODIFICARE:

Domanda 4: Le mie implementazioni funzionali consentono LCO (ottimizzazione dell'ultima chiamata, ovvero l'eliminazione della ricorsione della coda) e quindi evitare l'aggiunta di frame non necessari nello stack di chiamate?

Ho davvero cercato di implementare lo stesso algoritmo il più simile possibile nelle quattro lingue, anche se devo ammettere che la mia conoscenza di Haskell ed Erlang è molto limitata.


Codici sorgente utilizzati:

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

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

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

#! /usr/bin/env python3.2

import math

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

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

print (triangle)

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

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

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

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

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

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

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

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

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

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

main = print $ nextTriangle 1 1

55
@Jochen (e Seth) Non proprio che C sia veloce o fantastico, ma è percepito come facile scrivere codice performante (che potrebbe non essere vero, ma la maggior parte dei programmi sembra essere in grado, quindi abbastanza vero). Mentre esploro la mia risposta e ho scoperto che nel tempo sono vere, l'abilità del programmatore e la conoscenza delle ottimizzazioni comuni per la lingua scelta sono di grande importanza (specialmente per Haskell).
Thomas M. DuBuisson,

52
Appena controllato con Mathematica - ci vuole 0.25sec (con C ci vogliono 6sec qui), e il codice è solo: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. Evviva!
tsvikas,

35
C'è qualcun altro là fuori che ricorda queste guerre tra C e assemblea? "Certo! Puoi scrivere il tuo codice 10 volte più veloce in C, ma il tuo codice C può essere eseguito così in fretta? ..." Sono sicuro che sono state combattute le stesse battaglie tra codice macchina e assembly.
JS.

39
@JS: Probabilmente no, poiché assembly è semplicemente un insieme di mnemonici che digiti al posto del codice binario grezzo della macchina - normalmente c'è una corrispondenza 1-1 tra di loro.
Callum Rogers,

9
la conclusione, per Haskell: -O2 gli dà circa 3 volte uno speedup, e usando Int invece di Integer circa 4x-6x per lo speedup totale di 12x-14x e più.
Will Ness,

Risposte:


794

Usando GHC 7.0.3, gcc 4.4.6, Linux 2.6.29su un Core 2 Duo (2.5GHz) macchina x86_64, la compilazione utilizzando ghc -O2 -fllvm -fforce-recompper Haskell e gcc -O3 -lmper C.

  • La tua routine C viene eseguita in 8,4 secondi (più veloce della tua corsa probabilmente a causa di -O3)
  • La soluzione Haskell funziona in 36 secondi (a causa della -O2bandiera)
  • Il tuo factorCount'codice non viene digitato in modo esplicito e viene impostato automaticamente Integer(grazie a Daniel per aver corretto la mia diagnosi errata qui!). Dare una firma esplicita del tipo (che è comunque pratica standard) usando Inte il tempo cambia in 11,1 secondi
  • in factorCount'te hai inutilmente chiamato fromIntegral. Una correzione non comporta alcun cambiamento (il compilatore è intelligente, fortunato per te).
  • Hai usato moddove remè più veloce e sufficiente. Ciò modifica il tempo a 8,5 secondi .
  • factorCount'applica costantemente due argomenti aggiuntivi che non cambiano mai ( number, sqrt). Una trasformazione worker / wrapper ci dà:
 $ time ./so
 842161320  

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

Esatto, 7,95 secondi . Coerentemente mezzo secondo più veloce rispetto alla soluzione C . Senza la -fllvmbandiera sto ancora ottenendo 8.182 seconds, quindi il backend dell'NCG sta andando bene anche in questo caso.

Conclusione: Haskell è fantastico.

Codice risultante

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: Quindi ora che l'abbiamo esplorato, affrontiamo le domande

Domanda 1: erlang, python e haskell perdono velocità a causa dell'uso di numeri interi di lunghezza arbitraria o non purché i valori siano inferiori a MAXINT?

In Haskell, l'utilizzo Integerè più lento di Intma quanto più lentamente dipende dai calcoli eseguiti. Fortunatamente (per macchine a 64 bit) Intè sufficiente. Per motivi di portabilità, dovresti probabilmente riscrivere il mio codice da usare Int64o Word64(C non è l'unica lingua con a long).

Domanda 2: Perché l'hashell è così lento? C'è una bandiera del compilatore che spegne i freni o è la mia implementazione? (Quest'ultimo è abbastanza probabile poiché haskell è un libro con sette sigilli per me.)

Domanda 3: Potete offrirmi alcuni suggerimenti su come ottimizzare queste implementazioni senza cambiare il modo in cui determino i fattori? Ottimizzazione in qualsiasi modo: più bella, più veloce, più "nativa" della lingua.

Questo è quello che ho risposto sopra. La risposta è stata

  • 0) Utilizzare l'ottimizzazione tramite -O2
  • 1) Utilizzare tipi veloci (in particolare: unbox-grado) quando possibile
  • 2) remnon mod(un'ottimizzazione spesso dimenticata) e
  • 3) trasformazione worker / wrapper (forse l'ottimizzazione più comune).

Domanda 4: Le mie implementazioni funzionali consentono LCO e quindi evitano di aggiungere frame non necessari nello stack di chiamate?

Sì, non era questo il problema. Ottimo lavoro e felice di averlo considerato.


25
@Karl Perché in remrealtà è un sottocomponente moddell'operazione (non sono gli stessi). Se guardi nella libreria di GHC Base, vedi i modtest per diverse condizioni e regola il segno di conseguenza. (vedi modInt#in Base.lhs)
Thomas M. DuBuisson il

20
Un altro punto dati: ho scritto una rapida traduzione in Haskell del programma C senza guardare Haskell di @ Hyperboreus. Quindi è un po 'più vicino allo standard idiomatica Haskell, e l'unica ottimizzazione ho aggiunto deliberatamente sta sostituendo modcon remdopo aver letto questa risposta (eh, oops). Vedi il link per i miei tempi, ma la versione breve è "quasi identica alla C".
CA McCann,

106
Anche se la versione C funzionava più velocemente sulla mia macchina, ora ho un nuovo rispetto per Haskell. +1
Seth Carnegie,

11
Questo è abbastanza sorprendente per me, anche se devo ancora provarlo. Dato che l'originale factorCount'era ricorsivo della coda, avrei pensato che il compilatore potesse individuare i parametri extra che non venivano modificati e ottimizzare la ricorsione della coda solo per i parametri che cambiano (Haskell essendo un linguaggio puro dopo tutto, dovrebbe essere facile). Qualcuno pensa che il compilatore potrebbe farlo o dovrei tornare a leggere altri articoli teorici?
kizzx2

22
@ kizzx2: c'è un biglietto GHC da aggiungere. Da quanto ho capito, questa trasformazione può comportare allocazioni aggiuntive di oggetti di chiusura. Ciò significa prestazioni peggiori in alcuni casi, ma come suggerisce Johan Tibell nel suo post sul blog, questo può essere evitato se il wrapper risultante può essere incorporato.
Hammar

224

Ci sono alcuni problemi con l'implementazione di Erlang. Come base per quanto segue, il mio tempo di esecuzione misurato per il tuo programma Erlang non modificato è stato di 47,6 secondi, rispetto ai 12,7 secondi per il codice C.

La prima cosa da fare se si desidera eseguire il codice Erlang ad alta intensità computazionale è utilizzare il codice nativo. La compilazione erlc +native euler12ha ridotto il tempo a 41,3 secondi. Si tratta tuttavia di uno speedup molto più basso (solo il 15%) del previsto dalla compilazione nativa su questo tipo di codice e il problema è il tuo utilizzo -compile(export_all). Ciò è utile per la sperimentazione, ma il fatto che tutte le funzioni siano potenzialmente raggiungibili dall'esterno rende il compilatore nativo molto conservativo. (Il normale emulatore BEAM non è molto influenzato.) Sostituendo questa dichiarazione con si -export([solve/0]).ottiene una velocità molto migliore: 31,5 secondi (quasi il 35% dalla linea di base).

Ma il codice stesso ha un problema: per ogni iterazione nel ciclo factorCount, esegui questo test:

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

Il codice C non lo fa. In generale, può essere difficile fare un confronto equo tra diverse implementazioni dello stesso codice, e in particolare se l'algoritmo è numerico, perché è necessario essere sicuri che stiano effettivamente facendo la stessa cosa. Un leggero errore di arrotondamento in un'implementazione dovuto a qualche typecast da qualche parte può far sì che esegua molte più iterazioni rispetto all'altra anche se entrambi alla fine raggiungono lo stesso risultato.

Per eliminare questa possibile fonte di errore (e sbarazzarsi del test aggiuntivo in ogni iterazione), ho riscritto la funzione factorCount come segue, strettamente modellata sul codice 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.

Questa riscrittura, no export_alle compilazione nativa, mi ha dato il seguente tempo di esecuzione:

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

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

che non è poi così male rispetto al codice C:

$ time ./a.out 
842161320

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

considerando che Erlang non è affatto orientato alla scrittura di codice numerico, essendo solo il 50% più lento di C su un programma come questo è abbastanza buono.

Infine, per quanto riguarda le tue domande:

Domanda 1: erlang, python e haskell perdono velocità a causa dell'uso di numeri interi di lunghezza arbitraria o non finché i valori sono inferiori a MAXINT?

Sì, un po '. In Erlang, non c'è modo di dire "usa l'aritmetica a 32/64 bit con avvolgimento", quindi a meno che il compilatore non possa provare alcuni limiti sui tuoi numeri interi (e di solito non può), deve controllare tutti i calcoli per vedere se riescono a inserirsi in una sola parola taggata o se devono trasformarli in bignum allocati in heap. Anche se in pratica non vengono mai utilizzati bignum durante l'esecuzione, questi controlli dovranno essere eseguiti. D'altra parte, ciò significa che sai che l'algoritmo non fallirà mai a causa di un involucro intero imprevisto se gli dai improvvisamente input più grandi di prima.

Domanda 4: Le mie implementazioni funzionali consentono LCO e quindi evitano di aggiungere frame non necessari nello stack di chiamate?

Sì, il tuo codice Erlang è corretto rispetto all'ottimizzazione dell'ultima chiamata.


2
Sono d'accordo con te. Questo benchmark non è stato preciso in particolare per Erlang per una serie di motivi
Muzaaya Joshua,

156

Per quanto riguarda l'ottimizzazione di Python, oltre a utilizzare PyPy (per accelerazioni piuttosto impressionanti con zero modifiche al codice), è possibile utilizzare la toolchain di traduzione di PyPy per compilare una versione conforme a RPython o Cython per creare un modulo di estensione, entrambi che sono più veloci della versione C nei miei test, con il modulo Cython quasi due volte più veloce . Per riferimento includo anche i risultati dei benchmark C e PyPy:

C (compilato 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 l'ultima revisione di 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 versione di RPython ha un paio di cambiamenti chiave. Per tradurre in un programma autonomo devi definire il tuo target, che in questo caso è la mainfunzione. Si prevede che accetti sys.argvpoiché è solo argomento ed è necessario per restituire un int. Puoi tradurlo usando translate.py, % translate.py euler12-rpython.pyche si traduce in C e lo compila per te.

# 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 versione di Cython è stata riscritta come modulo di estensione _euler12.pyx, che io importazione e chiamo da un normale file Python. Il _euler12.pyxè essenzialmente lo stesso come la versione, con alcune dichiarazioni di tipo statico aggiuntivi. Setup.py ha il normale boilerplate per costruire l'estensione, 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 ho poca esperienza con RPython o Cython e sono rimasto piacevolmente sorpreso dai risultati. Se stai usando CPython, scrivere i tuoi bit di codice ad alta intensità di CPU in un modulo di estensione Cython sembra un modo davvero semplice per ottimizzare il tuo programma.


6
Sono curioso, la versione C può essere ottimizzata per essere almeno veloce come CPython?
Visualizza nome

4
@SargeBorsch che la versione di Cython è così veloce, perché si compila fino a una sorgente C altamente ottimizzata, il che significa che puoi sicuramente ottenere quella prestazione da C.
Eli Korvigo,

72

Domanda 3: Potete offrirmi alcuni suggerimenti su come ottimizzare queste implementazioni senza cambiare il modo in cui determino i fattori? Ottimizzazione in qualsiasi modo: più bella, più veloce, più "nativa" della lingua.

L'implementazione in C è subottimale (come suggerito da Thomas M. DuBuisson), la versione utilizza numeri interi a 64 bit (ovvero tipo di dati lungo ). Analizzerò l'elenco dell'assembly in seguito, ma con un'ipotesi istruita, ci sono alcuni accessi alla memoria in corso nel codice compilato, che rendono l'utilizzo di interi a 64 bit significativamente più lento. È quello o il codice generato (che si tratti di inserire meno di 64 bit in un registro SSE o di arrotondare un doppio a un intero a 64 bit è più lento).

Ecco il codice modificato (semplicemente sostituisco long con int e ho esplicitamente sottolineato factorCount, anche se non penso che ciò sia necessario 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);
}

Running + timing dà:

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

Per riferimento, l'implementazione haskell di Thomas nella risposta precedente fornisce:

$ 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

Conclusione: non togliendo nulla da ghc, è un ottimo compilatore, ma gcc normalmente genera codice più veloce.


22
Molto bella! Per fare un confronto, sulla mia macchina la tua soluzione C funziona 2.5 secondsmentre una modifica simile al codice Haskell (passando a Word32, aggiungendo il pragma INLINE) porta ad un tempo di esecuzione di 4.8 seconds. Forse qualcosa può essere fatto (non banalmente, a quanto pare) - il risultato gcc è certamente impressionante.
Thomas M. DuBuisson,

1
Grazie! Forse la domanda dovrebbe essere la velocità dell'output compilato da vari compilatori piuttosto che il linguaggio stesso. Poi di nuovo, tirando fuori i manuali Intel e ottimizzando a mano vincerai ancora del tutto (purché tu abbia le conoscenze e il tempo (molti)).
Raedwulf,

56

Dai un'occhiata a questo blog . Nel corso dell'ultimo anno ha affrontato alcuni dei problemi di Project Euler in Haskell e Python, e generalmente ha trovato Haskell molto più veloce. Penso che tra quelle lingue abbia più a che fare con la tua fluidità e il tuo stile di programmazione.

Quando si tratta della velocità di Python, stai usando un'implementazione sbagliata! Prova PyPy e per cose come questa troverai molto, molto più veloce.


32

L'implementazione di Haskell potrebbe essere notevolmente accelerata utilizzando alcune funzioni dei pacchetti Haskell. In questo caso ho usato primes, che è appena installato 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)

Tempi:

Il tuo programma originale:

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

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Implementazione migliorata

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

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Come puoi vedere, questo funziona in 38 millisecondi sulla stessa macchina in cui il tuo è stato eseguito in 16 secondi :)

Comandi di compilazione:

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

5
L'ultima volta che ho controllato, i "primi" di Haskell erano solo un enorme elenco di numeri primi precompilati - nessun calcolo, solo ricerca. Quindi sì, ovviamente questo sarà più veloce, ma non ti dice nulla sulla velocità computazionale della derivazione dei numeri primi in Haskell.
zxq9,

21
@ zxq9 potresti indicarmi dove si trova nella fonte del pacchetto primes ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ) l'elenco dei numeri primi?
Fraser,

4
Mentre la fonte mostra che i numeri primi non sono pre-calcolati, questa accelerazione è assolutamente folle, miglia più veloce della versione C, quindi cosa diavolo sta succedendo?
punto

1
Memorizzazione @semicolon. In questo caso, penso che Haskell abbia memorizzato tutti i numeri primi in fase di esecuzione, quindi non è necessario ricalcolarli ogni iterazione.
Hauleth,

5
Sono 1000 divisori, non 500.
Casper Færgemand,

29

Solo per divertimento. Quella che segue è un'implementazione Haskell più "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

Utilizzando ghc -O3, questo funziona costantemente in 0,55-0,58 secondi sulla mia macchina (1,73 GHz Core i7).

Una funzione factorCount più efficiente per la versione 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 long in ints in main, usando gcc -O3 -lm, questo funziona costantemente in 0,31-0,35 secondi.

Entrambi possono essere fatti funzionare ancora più velocemente se si sfrutta il fatto che l'ennesimo numero di triangolo = n * (n + 1) / 2 e n e (n + 1) hanno fattorizzazioni prime completamente disparate, quindi il numero di fattori di ogni metà può essere moltiplicato per trovare il numero di fattori dell'insieme. Il seguente:

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

ridurrà il tempo di esecuzione del codice c a 0,17-0,19 secondi e sarà in grado di gestire ricerche molto più grandi - più di 10000 fattori impiegano circa 43 secondi sulla mia macchina. Lascio una simile accelerazione haskell al lettore interessato.


3
Solo per confronto: versione c originale: 9.1690, versione di thaumkid: miglioramento 0.1060 86x.
Grazie

Wow. Haskell si esibisce alla grande una volta evitati i tipi inferiti
Piyush Katariya il

In realtà non è l'inferenza che l'ha fatto. Questo ti aiuta solo A) a eseguire il debug o evitare problemi di tipo e problemi di selezione delle istanze di typeclass B) eseguire il debug ed evitare alcuni problemi di tipo indecidibili con alcune estensioni di linguaggio moderne. Ti aiuta anche a rendere i tuoi programmi non comprensibili in modo da non poter mai aumentare i tuoi sforzi di sviluppo.
codeshot,

c versione 0.11 s su Intel skull canyon
codeshot

13
Domanda 1: erlang, python e haskell perdono velocità a causa dell'uso di numeri interi di lunghezza arbitraria o non finché i valori sono inferiori a MAXINT?

Questo è improbabile. Non posso dire molto su Erlang e Haskell (beh, forse un po 'su Haskell di seguito) ma posso indicare molti altri colli di bottiglia in Python. Ogni volta che il programma tenta di eseguire un'operazione con alcuni valori in Python, dovrebbe verificare se i valori provengono dal tipo corretto e costa un po 'di tempo. La tua factorCountfunzione alloca un elenco con più range (1, isquare + 1)volte e l' mallocallocazione della memoria in fase di runtime è molto più lenta rispetto all'iterazione su un intervallo con un contatore come factorCount()avviene in C. In particolare, viene chiamata più volte e alloca quindi un sacco di elenchi. Inoltre, non dimentichiamo che Python è interpretato e l'interprete CPython non ha grande attenzione all'ottimizzazione.

EDIT : oh, beh, noto che stai usando Python 3, quindi range()non restituisce un elenco, ma un generatore. In questo caso, il mio punto sull'allocazione di elenchi è per metà sbagliato: la funzione alloca solo rangeoggetti, che sono comunque inefficienti ma non inefficienti come allocare un elenco con molti elementi.

Domanda 2: Perché l'hashell è così lento? C'è una bandiera del compilatore che spegne i freni o è la mia implementazione? (Quest'ultimo è abbastanza probabile poiché haskell è un libro con sette sigilli per me.)

Stai usando gli abbracci ? Hugs è un interprete molto lento. Se lo stai usando, forse potresti ottenere un momento migliore con GHC - ma sto solo cogitando l'ipotesi, il tipo di cose che un buon compilatore di Haskell fa sotto il cofano è piuttosto affascinante e ben oltre la mia comprensione :)

Domanda 3: Potete offrirmi alcuni suggerimenti su come ottimizzare queste implementazioni senza cambiare il modo in cui determino i fattori? Ottimizzazione in qualsiasi modo: più bella, più veloce, più "nativa" della lingua.

Direi che stai giocando a un gioco strano. La parte migliore della conoscenza di varie lingue è di usarle nel modo più diverso possibile :) Ma sto divagando, non ho alcuna raccomandazione per questo punto. Spiacenti, spero che qualcuno ti possa aiutare in questo caso :)

Domanda 4: Le mie implementazioni funzionali consentono LCO e quindi evitano di aggiungere frame non necessari nello stack di chiamate?

Per quanto mi ricordo, devi solo assicurarti che la tua chiamata ricorsiva sia l'ultimo comando prima di restituire un valore. In altre parole, una funzione come quella qui sotto potrebbe utilizzare tale ottimizzazione:

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

Tuttavia, non avresti tale ottimizzazione se la tua funzione fosse come quella qui sotto, perché c'è una operazione (moltiplicazione) dopo la chiamata ricorsiva:

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

Ho separato le operazioni in alcune variabili locali per chiarire quali operazioni vengono eseguite. Tuttavia, il più comune è vedere queste funzioni come di seguito, ma sono equivalenti per il punto che sto sottolineando:

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

Nota che spetta al compilatore / interprete decidere se eseguirà la ricorsione della coda. Ad esempio, l'interprete Python non lo fa se ricordo bene (ho usato Python nel mio esempio solo per la sua fluente sintassi). Ad ogni modo, se trovi cose strane come funzioni fattoriali con due parametri (e uno dei parametri ha nomi come acc, accumulatorecc.) Ora sai perché la gente lo fa :)


@Hyperboreus grazie! Inoltre, sono davvero curioso delle tue prossime domande. Tuttavia, ti avverto che la mia conoscenza è limitata, quindi non ho potuto rispondere a tutte le tue domande. Per cercare di compensarlo ho creato la mia wiki della community di risposta in modo che le persone possano completarlo più facilmente.
brandizzi,

Informazioni sull'uso dell'intervallo. Quando sostituisco l'intervallo con un ciclo while con incremento (imitando il ciclo for di C), il tempo di esecuzione effettivamente raddoppia. Immagino che i generatori siano abbastanza ottimizzati.
Iperboro

12

Con Haskell, non hai davvero bisogno di pensare esplicitamente alle ricorsioni.

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

Nel codice sopra, ho sostituito le ricorsioni esplicite nella risposta di @Thomas con operazioni di elenco comuni. Il codice fa ancora esattamente la stessa cosa senza di noi preoccuparci della ricorsione della coda. Funziona (~ 7.49s ) circa il 6% più lentamente rispetto alla versione nella risposta di @Thomas (~ 7.04s ) sulla mia macchina con GHC 7.6.2, mentre la versione C di @Raedwulf esegue ~ 3.15s . Sembra che GHC sia migliorato nel corso dell'anno.

PS. So che è una vecchia domanda, e mi imbatto in ricerche di Google (ho dimenticato quello che stavo cercando, ora ...). Volevo solo commentare la domanda su LCO ed esprimere i miei sentimenti su Haskell in generale. Volevo commentare la risposta principale, ma i commenti non consentono blocchi di codice.


9

Alcuni altri numeri e spiegazioni per la versione C. Apparentemente nessuno lo ha fatto in tutti quegli anni. Ricorda di votare questa risposta in modo che tutti possano vedere e imparare.

Primo passo: benchmark dei programmi dell'autore

Specifiche del laptop:

  • CPU i3 M380 (931 MHz - modalità risparmio batteria massimo)
  • 4 GB di memoria
  • Win7 64 bit
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin con gcc 4.9.3
  • Python 2.7.10

comandi:

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

I nomi dei file sono: integertype_architecture_compiler.exe

  • integertype è lo stesso del programma originale per ora (ne parleremo più avanti)
  • l'architettura è x86 o x64 a seconda delle impostazioni del compilatore
  • il compilatore è gcc o vs2012

Fase due: indagare, migliorare e eseguire nuovamente il benchmark

VS è il 250% più veloce di gcc. I due compilatori dovrebbero dare una velocità simile. Ovviamente, qualcosa non va nel codice o nelle opzioni del compilatore. Investighiamo!

Il primo punto di interesse sono i tipi interi. Le conversioni possono essere costose e la coerenza è importante per una migliore generazione e ottimizzazione del codice. Tutti i numeri interi devono essere dello stesso tipo.

È un pasticcio misto di inte longproprio ora. Lo miglioreremo. Che tipo da usare? Il più veloce. Devo confrontarli tutti!

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

I tipi interi sono int long int32_t uint32_t int64_te uint64_tda#include <stdint.h>

Ci sono MOLTI tipi di numeri interi in C, oltre ad alcuni firmati / non firmati con cui giocare, oltre alla scelta di compilare come x86 o x64 (da non confondere con la dimensione intera effettiva). Sono molte le versioni da compilare ed eseguire ^^

Terzo passaggio: comprensione dei numeri

Conclusioni definitive:

  • Gli interi a 32 bit sono ~ 200% più veloci degli equivalenti a 64 bit
  • gli interi senza segno a 64 bit sono il 25% più veloci dei 64 bit con segno (sfortunatamente, non ho spiegazioni per questo)

Domanda a sorpresa: "Quali sono le dimensioni di int e long in C?"
La risposta giusta è: le dimensioni di int e long in C non sono ben definite!

Dalle specifiche C:

int è lungo almeno 32 bit
è almeno un int

Dalla pagina man di gcc (flag -m32 e -m64):

L'ambiente a 32 bit imposta int, long e pointer su 32 bit e genera codice che gira su qualsiasi sistema i386.
L'ambiente a 64 bit imposta int su 32 bit e long e puntatore su 64 bit e genera codice per l'architettura x86-64 di AMD.

Dalla documentazione MSDN (Data Type Ranges) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 byte, noto anche come
long firmato , 4 byte, noto anche come long int e firmato long int

Per concludere: lezioni apprese

  • Gli interi a 32 bit sono più veloci degli interi a 64 bit.

  • I tipi di numeri interi standard non sono ben definiti in C né C ++, variano a seconda dei compilatori e delle architetture. Quando sono necessari coerenza e prevedibilità, utilizzare la uint32_tfamiglia di numeri interi da #include <stdint.h>.

  • Problemi di velocità risolti. Tutte le altre lingue sono indietro del cento per cento, C & C ++ vincono ancora! Lo fanno sempre. Il prossimo miglioramento sarà il multithreading utilizzando OpenMP: D


Per curiosità, come fanno i compilatori Intel? Di solito sono davvero bravi a ottimizzare il codice numerico.
Kirbyfan64sos,

Dove trovi un riferimento che dice che la specifica C garantisce "int è almeno 32 bit"? Le uniche garanzie che conosco sono le magnitudini minime di INT_MINe INT_MAX(-32767 e 32767, che praticamente impongono un requisito intdi almeno 16 bit). longdeve essere almeno grande quanto un inte la media dei requisiti di intervallo longè di almeno 32 bit.
ShadowRanger il


8

Guardando l'implementazione di Erlang. Il tempismo ha incluso l'avvio dell'intera macchina virtuale, l'esecuzione del programma e l'arresto della macchina virtuale. Sono abbastanza sicuro che l'installazione e l'arresto di erlang vm richiede del tempo.

Se il tempismo fosse eseguito all'interno della stessa macchina virtuale erlang, i risultati sarebbero diversi poiché in quel caso avremmo il tempo effettivo solo per il programma in questione. Altrimenti, credo che il tempo totale impiegato dal processo di avvio e caricamento di Erlang Vm più quello di fermarlo (come lo metti nel tuo programma) siano tutti inclusi nel tempo totale che il metodo che stai usando per cronometrare il programma in uscita. Prendi in considerazione l'utilizzo del tempismo erlang stesso che utilizziamo quando vogliamo programmare i nostri programmi all'interno della macchina virtuale stessa timer:tc/1 or timer:tc/2 or timer:tc/3. In questo modo, i risultati di erlang escluderanno il tempo impiegato per l'avvio e l'arresto / arresto / arresto della macchina virtuale. Questo è il mio ragionamento lì, pensaci e poi prova di nuovo il tuo punto di riferimento.

In realtà suggerisco di provare a programmare il programma (per le lingue che hanno un tempo di esecuzione), all'interno del tempo di esecuzione di quelle lingue al fine di ottenere un valore preciso. C, ad esempio, non ha costi generali di avvio e chiusura di un sistema di runtime, così come Erlang, Python e Haskell (sicuro al 98% di questo - sostengo la correzione). Quindi (sulla base di questo ragionamento) concludo dicendo che questo benchmark non era abbastanza preciso / equo per le lingue in esecuzione su un sistema di runtime. Facciamolo di nuovo con queste modifiche.

EDIT: inoltre, anche se tutte le lingue avessero sistemi di runtime, il sovraccarico di avviarne ognuna e interromperla sarebbe diverso. quindi suggerisco di passare il tempo dall'interno dei sistemi di runtime (per le lingue per cui questo vale). La VM Erlang è nota per avere un notevole sovraccarico all'avvio!


Ho dimenticato di menzionarlo nel mio post, ma ho misurato il tempo necessario solo per avviare il sistema (erl -noshell -s erlang halt) - circa 0,1 secondi sulla mia macchina. Questo è abbastanza piccolo rispetto al tempo di esecuzione del programma (circa 10 secondi) di cui non vale la pena cavillare.
RichardC,

sulla tua macchina! non sappiamo se stai lavorando su un server Sun Fire !. Dal momento che il tempo è una variabile proporzionale alle specifiche della macchina, dovrebbe essere preso in considerazione .... cavilli?
Muzaaya Joshua,

2
@RichardC Da nessuna parte è stato detto che Erlang è più veloce :) Ha obiettivi diversi, non velocità!
Eccezione

7

Domanda 1: Erlang, Python e Haskell perdono velocità a causa dell'uso di numeri interi di lunghezza arbitraria o non purché i valori siano inferiori a MAXINT?

Alla prima domanda si può rispondere negativamente per Erlang. All'ultima domanda si risponde utilizzando Erlang in modo appropriato, come in:

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

Dal momento che è più veloce del tuo esempio C iniziale, immagino che ci siano numerosi problemi come altri hanno già trattato in dettaglio.

Questo modulo Erlang viene eseguito su un netbook economico in circa 5 secondi ... Utilizza il modello di thread di rete in erlang e, come tale, dimostra come trarre vantaggio dal modello di eventi. Potrebbe essere distribuito su molti nodi. Ed è veloce. Non è il mio codice.

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

server() ->  
  server(1).

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

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

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

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

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

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

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

Il test seguente si è svolto su una CPU Intel (R) Atom (TM) N270 a 1,60 GHz

~$ time erl -noshell -s p12dist start

The result is: 76576500.

^C

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

real    0m5.510s
user    0m5.836s
sys 0m0.152s

aumentando il valore a 1000 come di seguito non si ottiene il risultato corretto. Con> 500 come sopra, test più recente: CPU IntelCore2 6600 a 2,40 GHz in reali 0m2.370s
Mark Washeim

il tuo risultato: 76576500 tutti gli altri: 842161320 c'è qualcosa di sbagliato con il tuo risultato
davidDavidson

Da quando ho avuto qualche altro problema con Eulero, ho appena controllato il mio risultato. La risposta a projecteuler.net/problem=12 è 76576500 senza dubbio. So che sembra strano, ma ho appena controllato.
Mark Washeim,

Per confronto ottengo 9.03 con la versione c originale mentre usando Erlang 19 con il codice di Mark ottengo 5.406, 167.0366% più veloce.
Grazie

5

C ++ 11, <20ms per me - Eseguilo qui

Comprendo che desideri suggerimenti per migliorare la tua conoscenza specifica della lingua, ma dato che qui è stato ben trattato, ho pensato di aggiungere un contesto per le persone che potrebbero aver guardato il commento di matematica sulla tua domanda, ecc., E mi chiedevo perché questo il codice era molto più lento.

Questa risposta serve principalmente a fornire un contesto per aiutare le persone a valutare più facilmente il codice nella tua domanda / altre risposte.

Questo codice utilizza solo un paio di (brutte) ottimizzazioni, non correlate al linguaggio utilizzato, basate su:

  1. ogni numero di traingle ha la forma n (n + 1) / 2
  2. n e n + 1 sono coprimi
  3. il numero di divisori è una funzione moltiplicativa

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

In media ci vogliono circa 19ms per il mio desktop e 80ms per il mio laptop, molto lontani dalla maggior parte degli altri codici che ho visto qui. E ci sono, senza dubbio, molte ottimizzazioni ancora disponibili.


7
Questo non è esplicitamente richiesto da chi l'ha chiesto, "Ho davvero cercato di implementare lo stesso algoritmo il più simile possibile nelle quattro lingue". Per citare un commento su una delle tante risposte eliminate simili alle tue "è abbastanza ovvio che puoi ottenere velocità più elevate con un algoritmo migliore indipendentemente dalla lingua".
Thomas M. DuBuisson,

2
@ ThomasM.DuBuisson. Ecco a cosa sto arrivando. La domanda \ risposte implica che le accelerazioni algoritmiche sono significative (e ovviamente l'OP non le chiede), ma non esiste un esempio esplicito. Penso che questa risposta - che non è esattamente un codice fortemente ottimizzato - fornisce un piccolo contesto utile per chiunque, come me, che si chiedesse quanto fosse lento / veloce il codice dell'OP.
user3125280,

gcc può anche pre-calcolare molti modelli. int a = 0; per (int i = 0; i <10000000; ++ i) {a + = i;} sarà calcolato al momento della compilazione, quindi prendi <1ms in fase di esecuzione. Anche questo conta
Arthur

@Thomas: Devo essere d'accordo con user3125280 - i linguaggi dovrebbero essere confrontati su come fanno a fare qualcosa di intelligente invece di come non riescono a battere un vero linguaggio di programmazione nel fare qualcosa di stupido. Gli algoritmi intelligenti di solito si preoccupano meno dell'efficienza microscopica che della flessibilità, della capacità di collegare le cose (combinarle) e dell'infrastruttura. Il punto non è tanto se si ha 20 ms o 50 ms, è non sempre 8 secondi o 8 minuti.
DarthGizka,

5

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

Ottengo:

versione c originale: 9.1690 100%
go: 8.2520 111%

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

Ottengo:

versione c originale: 9.1690 100%
versione c di thaumkid: 0.1060 8650%
prima versione go: 8.2520 111%
seconda versione go: 0.0230 39865%

Ho anche provato Python3.6 e pypy3.3-5.5-alpha:

versione c originale: 8.629 100%
versione c di thaumkid: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

e poi con il seguente codice ho ottenuto:

versione c originale: 8.629 100%
versione c di 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)

1

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

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

Questo produrrà la risposta corretta per l'esempio multi-processo di Erlang.


2
Era inteso come un commento a questa risposta ? Perché non è chiaro, e questa non è una risposta da sola.
ShadowRanger,

1

Ho ipotizzato che il numero di fattori sia grande solo se i numeri coinvolti hanno molti piccoli fattori. Quindi ho usato l'eccellente algoritmo di thaumkid, ma prima ho usato un'approssimazione al conteggio dei fattori che non è mai troppo piccola. È abbastanza semplice: controlla i fattori primi fino a 29, quindi controlla il numero rimanente e calcola un limite superiore per la quantità di fattori. Usa questo per calcolare un limite superiore per il numero di fattori e se quel numero è abbastanza alto, calcola il numero esatto di fattori.

Il codice seguente non ha bisogno di questo presupposto per correttezza, ma per essere veloce. Sembra funzionare; solo circa uno su 100.000 numeri fornisce una stima sufficientemente elevata da richiedere un controllo completo.

Ecco il codice:

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

Questo trova il 14.753.024 ° triangolare con 13824 fattori in circa 0,7 secondi, l'879.207.615 ° numero triangolare con 61.440 fattori in 34 secondi, il 12.524.486.975 ° numero triangolare con 138.240 fattori in 10 minuti 5 secondi e il 26.467.792.064 ° numero triangolare con 172.032 fattori in 21 minuti e 25 secondi (2.4 GHz Core2 Duo), quindi questo codice richiede in media solo 116 cicli del processore per numero. L'ultimo numero triangolare stesso è maggiore di 2 ^ 68, quindi


0

Ho modificato la versione "Jannich Brendle" a 1000 anziché a 500. E ho elencato il risultato di euler12.bin, euler12.erl, p12dist.erl. Entrambi i codici erl usano '+ native' per compilare.

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

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

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

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

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

int factorCount (long n)
{
    double square = sqrt (n);
    int isquare = (int) square+1;
    long candidate = 2;
    int count = 1;
    while(candidate <= isquare && candidate<=n){
        int c = 1;
        while (n % candidate == 0) {
           c++;
           n /= candidate;
        }
        count *= c;
        candidate++;
    }
    return count;
}

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

gcc -lm -Ofast euler.c

time ./a.out

2.79s utente 0.00s sistema 99% cpu 2.794 totale

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.