Interpretazione di un benchmark in C, Clojure, Python, Ruby, Scala e altri [chiuso]


91

Disclaimer

So che i benchmark artificiali sono malvagi. Possono mostrare risultati solo per situazioni ristrette molto specifiche. Non presumo che una lingua sia migliore dell'altra a causa di qualche stupida panchina. Tuttavia mi chiedo perché i risultati siano così diversi. Si prega di vedere le mie domande in fondo.

Descrizione del benchmark matematico

Il benchmark consiste in semplici calcoli matematici per trovare coppie di numeri primi che differiscono di 6 (i cosiddetti numeri primi sexy ) Ad esempio, numeri primi sexy inferiori a 100 sarebbero:(5 11) (7 13) (11 17) (13 19) (17 23) (23 29) (31 37) (37 43) (41 47) (47 53) (53 59) (61 67) (67 73) (73 79) (83 89) (97 103)

Tabella dei risultati

Nella tabella: tempo di calcolo in secondi In esecuzione: tutto tranne Factor era in esecuzione in VirtualBox (guest amd64 instabile Debian, host Windows 7 x64) CPU: AMD A4-3305M

  Sexy primes up to:        10k      20k      30k      100k               

  Bash                    58.00   200.00     [*1]      [*1]

  C                        0.20     0.65     1.42     15.00

  Clojure1.4               4.12     8.32    16.00    137.93

  Clojure1.4 (optimized)   0.95     1.82     2.30     16.00

  Factor                    n/a      n/a    15.00    180.00

  Python2.7                1.49     5.20    11.00       119     

  Ruby1.8                  5.10    18.32    40.48    377.00

  Ruby1.9.3                1.36     5.73    10.48    106.00

  Scala2.9.2               0.93     1.41     2.73     20.84

  Scala2.9.2 (optimized)   0.32     0.79     1.46     12.01

[* 1] - Ho paura di immaginare quanto tempo ci vorrà

Elenchi di codici

C:

int isprime(int x) {
  int i;
  for (i = 2; i < x; ++i)
    if (x%i == 0) return 0;
  return 1;
}

void findprimes(int m) {
  int i;
  for ( i = 11; i < m; ++i)
    if (isprime(i) && isprime(i-6))
      printf("%d %d\n", i-6, i);
}

main() {
    findprimes(10*1000);
}

Rubino:

def is_prime?(n)
  (2...n).all?{|m| n%m != 0 }
end

def sexy_primes(x)
  (9..x).map do |i|
    [i-6, i]
  end.select do |j|
    j.all?{|j| is_prime? j}
  end
end

a = Time.now
p sexy_primes(10*1000)
b = Time.now
puts "#{(b-a)*1000} mils"

Scala:

def isPrime(n: Int) =
  (2 until n) forall { n % _ != 0 }

def sexyPrimes(n: Int) = 
  (11 to n) map { i => List(i-6, i) } filter { _ forall(isPrime(_)) }

val a = System.currentTimeMillis()
println(sexyPrimes(100*1000))
val b = System.currentTimeMillis()
println((b-a).toString + " mils")

Scala ottimizzata isPrime(la stessa idea dell'ottimizzazione di Clojure):

import scala.annotation.tailrec

@tailrec // Not required, but will warn if optimization doesn't work
def isPrime(n: Int, i: Int = 2): Boolean = 
  if (i == n) true 
  else if (n % i != 0) isPrime(n, i + 1)
  else false

Clojure:

(defn is-prime? [n]
  (every? #(> (mod n %) 0)
    (range 2 n)))

(defn sexy-primes [m]
  (for [x (range 11 (inc m))
        :let [z (list (- x 6) x)]
        :when (every? #(is-prime? %) z)]
      z))

(let [a (System/currentTimeMillis)]
  (println (sexy-primes (* 10 1000)))
  (let [b (System/currentTimeMillis)]
    (println (- b a) "mils")))

Clojure ottimizzato is-prime?:

(defn ^:static is-prime? [^long n]
  (loop [i (long 2)] 
    (if (= (rem n i) 0)
      false
      (if (>= (inc i) n) true (recur (inc i))))))

Pitone

import time as time_

def is_prime(n):
  return all((n%j > 0) for j in xrange(2, n))

def primes_below(x):
  return [[j-6, j] for j in xrange(9, x+1) if is_prime(j) and is_prime(j-6)]

a = int(round(time_.time() * 1000))
print(primes_below(10*1000))
b = int(round(time_.time() * 1000))
print(str((b-a)) + " mils")

Fattore

MEMO:: prime? ( n -- ? )
n 1 - 2 [a,b] [ n swap mod 0 > ] all? ;

MEMO: sexyprimes ( n n -- r r )
[a,b] [ prime? ] filter [ 6 + ] map [ prime? ] filter dup [ 6 - ] map ;

5 10 1000 * sexyprimes . .

Bash (zsh):

#!/usr/bin/zsh
function prime {
  for (( i = 2; i < $1; i++ )); do
    if [[ $[$1%i] == 0 ]]; then
      echo 1
      exit
    fi
  done
  echo 0
}

function sexy-primes {
  for (( i = 9; i <= $1; i++ )); do
    j=$[i-6]
    if [[ $(prime $i) == 0 && $(prime $j) == 0 ]]; then
      echo $j $i
    fi
  done
}

sexy-primes 10000

Domande

  1. Perché Scala è così veloce? È a causa della digitazione statica ? O sta semplicemente usando JVM in modo molto efficiente?
  2. Perché una così grande differenza tra Ruby e Python? Pensavo che questi due non fossero in qualche modo totalmente diversi. Forse il mio codice è sbagliato. Per favore, illuminami! Grazie. UPD Sì, era un errore nel mio codice. Python e Ruby 1.9 sono abbastanza uguali.
  3. Un salto di produttività davvero impressionante tra le versioni di Ruby.
  4. Posso ottimizzare il codice Clojure aggiungendo dichiarazioni di tipo? Aiuterà?

6
@mgilson in realtà è all'altezza, sqrt(n)ma il calcolo può richiedere del tempo. Inoltre il codice C stampa i numeri primi non appena li trova, mentre gli altri linguaggi li calcolano in elenchi e poi li stampa. Sebbene C sia non sorprendentemente il più veloce, potresti essere in grado di ottenerlo più velocemente.
Russ

2
(E ovviamente il Setaccio di Eratostene .. ma questo micro benchmark è praticamente uno stress test di iterazione e operazioni matematiche. Tuttavia, non sono ancora "giusti" in quanto in alcuni sono più pigri.)

2
Ho appena eseguito sia la mia versione Go che la tua versione C (che si assomigliano molto) e ho praticamente ottenuto la stessa velocità in entrambe. Ho provato solo la versione 100k: C: 2.723s Go: 2.743s.
Sebastián Grignoli

3
Non è necessario eseguire calcoli sqrtper questo controllo. Puoi calcolare il quadrato di icome infor (i = 2; i * i <= x; ++i) ...
ivant

3
Ti suggerisco di annotare Scala ottimizzata isPrimecon @tailrec, per assicurarti di utilizzare la ricorsione in coda. È facile fare erroneamente qualcosa che impedisce la ricorsione della coda e questa annotazione dovrebbe avvisarti se ciò accade.
Daniel C. Sobral

Risposte:


30

Risposte approssimative:

  1. La digitazione statica di Scala lo sta aiutando parecchio qui - questo significa che usa la JVM in modo abbastanza efficiente senza troppi sforzi extra.
  2. Non sono esattamente sicuro della differenza Ruby / Python, ma sospetto che (2...n).all?la funzione is-prime?sia probabilmente ottimizzata abbastanza bene in Ruby (EDIT: sembra che sia proprio così, vedi la risposta di Julian per maggiori dettagli ...)
  3. Ruby 1.9.3 è semplicemente ottimizzato molto meglio
  4. Il codice Clojure può sicuramente essere accelerato molto! Sebbene Clojure sia dinamico per impostazione predefinita, puoi utilizzare suggerimenti sul tipo, matematica primitiva ecc. Per avvicinarti alla velocità di Scala / Java puro in molti casi quando ne hai bisogno.

L'ottimizzazione più importante nel codice Clojure sarebbe quella di utilizzare la matematica primitiva tipizzata all'interno is-prime?, qualcosa come:

(set! *unchecked-math* true) ;; at top of file to avoid using BigIntegers

(defn ^:static is-prime? [^long n]
  (loop [i (long 2)] 
    (if (zero? (mod n i))
      false
      (if (>= (inc i) n) true (recur (inc i))))))

Con questo miglioramento, ottengo Clojure che completa 10k in 0.635 secondi (cioè il secondo più veloce nella tua lista, battendo Scala)

PS nota che in alcuni casi hai il codice di stampa all'interno del tuo benchmark - non è una buona idea in quanto distorcerebbe i risultati, specialmente se l'uso di una funzione come printper la prima volta causa l'inizializzazione dei sottosistemi IO o qualcosa del genere!


2
Non penso che la parte su Ruby e Python sia necessariamente vera, ma altrimenti +1 ..

La digitazione non ha mostrato alcun risultato stabile misurabile, ma il tuo nuovo is-prime?mostra un miglioramento doppio. ;)
defhlt

non potrebbe essere reso più veloce se ci fosse un mod non controllato?
Hendekagon

1
@Hendekagon - probabilmente! non sono sicuro di quanto questo venga ottimizzato dall'attuale compilatore Clojure, probabilmente c'è spazio per miglioramenti. Clojure 1.4 sicuramente aiuta molto in generale per questo tipo di cose, 1.5 sarà probabilmente anche migliore.
mikera

1
(zero? (mod n i))dovrebbe essere più veloce di(= (mod n i) 0)
Jonas

23

Ecco una versione veloce di Clojure, che utilizza gli stessi algoritmi di base:

(set! *unchecked-math* true)

(defn is-prime? [^long n]
  (loop [i 2]
    (if (zero? (unchecked-remainder-int n i))
      false
      (if (>= (inc i) n)
        true
        (recur (inc i))))))

(defn sexy-primes [m]
  (for [x (range 11 (inc m))
        :when (and (is-prime? x) (is-prime? (- x 6)))]
    [(- x 6) x]))

Funziona circa 20 volte più velocemente dell'originale sulla mia macchina. Ed ecco una versione che sfrutta la nuova libreria di riduttori in 1.5 (richiede Java 7 o JSR 166):

(require '[clojure.core.reducers :as r]) ;'

(defn sexy-primes [m]
  (->> (vec (range 11 (inc m)))
       (r/filter #(and (is-prime? %) (is-prime? (- % 6))))
       (r/map #(list (- % 6) %))
       (r/fold (fn ([] []) ([a b] (into a b))) conj)))

Funziona circa 40 volte più velocemente dell'originale. Sulla mia macchina, sono 100.000 in 1,5 secondi.


2
Usando unchecked-remainder-into semplicemente al remposto di modrisultati di digitazione statica per aumentare le prestazioni di 4x. Bello!
defhlt

22

Risponderò solo # 2, dal momento che è l'unico che ho qualcosa di anche lontanamente intelligente da dire, ma per il codice Python, si sta creando una lista intermedia in is_prime, mentre si sta utilizzando .mapnel vostro allin Ruby che è solo iterazione.

Se cambi il tuo is_primein:

def is_prime(n):
    return all((n%j > 0) for j in range(2, n))

sono alla pari.

Potrei ottimizzare ulteriormente Python, ma il mio Ruby non è abbastanza buono da sapere quando ho dato un vantaggio maggiore (ad esempio, l'uso xrangefa vincere Python sulla mia macchina, ma non ricordo se l'intervallo Ruby che hai usato crea un intero intervallo in memoria o meno).

EDIT: senza essere troppo sciocco, facendo apparire il codice Python come:

import time

def is_prime(n):
    return all(n % j for j in xrange(2, n))

def primes_below(x):
    return [(j-6, j) for j in xrange(9, x + 1) if is_prime(j) and is_prime(j-6)]

a = int(round(time.time() * 1000))
print(primes_below(10*1000))
b = int(round(time.time() * 1000))
print(str((b-a)) + " mils")

il che non cambia molto di più, lo mette a 1,5 secondi per me e, essendo estremamente sciocco, eseguirlo con PyPy lo mette a .3s per 10K e 21s per 100K.


1
Il generatore fa una grande differenza qui in quanto consente alla funzione di salvarsi al primo False(buona cattura).
mgilson

Non vedo l'ora che diventino insensibili in PyPy ... Sarà fantastico.
mgilson

Faresti per favore la mia risposta in PyPy? Sono curioso di sapere quanto sarebbe più veloce.
steveha

1
Hai assolutamente ragione sia sull'iterazione che su xrange! Ho risolto e ora Python e Ruby mostrano risultati uguali.
defhlt

1
@steveha Lo farò solo se prometti di uscire e scaricare PyPy tu stesso :)! pypy.org/download.html ha i binari per tutti i sistemi operativi comuni e il tuo gestore di pacchetti ce l' ha senza dubbio. Comunque, come per il tuo benchmark, con lru_cacheun'implementazione casuale per 2.7 trovata su AS, 100K gira in 2.3s.
Julian

16

Puoi rendere la Scala molto più veloce modificando il tuo isPrimemetodo in

  def isPrime(n: Int, i: Int = 2): Boolean = 
    if (i == n) true 
    else if (n % i != 0) isPrime(n, i + 1)
    else false

Non così conciso ma il programma viene eseguito nel 40% delle volte!

Ritagliamo gli oggetti superflui Rangee anonimi Function, il compilatore Scala riconosce la ricorsione in coda e la trasforma in un ciclo while, che la JVM può trasformare in codice macchina più o meno ottimale, quindi non dovrebbe essere troppo lontano dal C versione.

Vedi anche: Come ottimizzare le comprensioni e i loop in Scala?


2
2x miglioramento. E bel collegamento!
defhlt

btw questo metodo corpo è identico a i == n || n % i != 0 && isPrime(n, i + 1), che è più breve, anche se un po 'più difficile da leggere
Luigi Plinge

1
Avresti dovuto aggiungere l' @tailrecannotazione, per assicurarti che effettuerà quell'ottimizzazione.
Daniel C. Sobral

8

Ecco la mia versione scala sia parallela che non parallela, solo per divertimento: (Nel mio calcolo dual core, la versione parallela richiede 335 ms mentre la versione no-parallela richiede 655 ms)

object SexyPrimes {
  def isPrime(n: Int): Boolean = 
    (2 to math.sqrt(n).toInt).forall{ n%_ != 0 }

  def isSexyPrime(n: Int): Boolean = isPrime(n) && isPrime(n-6)

  def findPrimesPar(n: Int) {
    for(k <- (11 to n).par)
      if(isSexyPrime(k)) printf("%d %d\n",k-6,k)
  }

  def findPrimes(n: Int) {
    for(k <- 11 to n)
      if(isSexyPrime(k)) printf("%d %d\n",k-6,k)
  }


  def timeOf(call : =>Unit) {
    val start = System.currentTimeMillis
    call
    val end = System.currentTimeMillis
    println((end-start)+" mils")
  }

  def main(args: Array[String]) {
    timeOf(findPrimes(100*1000))
    println("------------------------")
    timeOf(findPrimesPar(100*1000))
  }
}

EDIT: Secondo il suggerimento di Emil H , ho cambiato il mio codice per evitare gli effetti di IO e jvm warmup:

Il risultato mostra nel mio calcolo:

Elenco (3432, 1934, 3261, 1716, 3229, 1654, 3214, 1700)

object SexyPrimes {
  def isPrime(n: Int): Boolean = 
    (2 to math.sqrt(n).toInt).forall{ n%_ != 0 }

  def isSexyPrime(n: Int): Boolean = isPrime(n) && isPrime(n-6)

  def findPrimesPar(n: Int) {
    for(k <- (11 to n).par)
      if(isSexyPrime(k)) ()//printf("%d %d\n",k-6,k)
  }

  def findPrimes(n: Int) {
    for(k <- 11 to n)
      if(isSexyPrime(k)) ()//printf("%d %d\n",k-6,k)
  }


  def timeOf(call : =>Unit): Long = {
    val start = System.currentTimeMillis
    call
    val end = System.currentTimeMillis
    end - start 
  }

  def main(args: Array[String]) {
    val xs = timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::Nil
    println(xs)
  }
}

1
Il codice è influenzato dal riscaldamento jvm? Ad esempio, isSexyPrimepotrebbe essere (più) ottimizzato quando chiamato da findPrimesPare non tanto quando chiamato dafindPrimes
Emil H

@ EmilH Abbastanza giusto. Ho cambiato il mio codice per evitare l'effetto del riscaldamento io e jvm.
Eastsun

Solo salire a sqrt (n) è una buona ottimizzazione, ma ora stai valutando un algoritmo diverso.
Luigi Plinge

7

Non importa i parametri di riferimento; il problema mi ha interessato e ho apportato alcune modifiche veloci. Questo utilizza il lru_cachedecoratore, che memorizza una funzione; quindi, quando chiamiamo is_prime(i-6), praticamente riceviamo quel primo assegno gratuitamente. Questa modifica taglia il lavoro all'incirca della metà. Inoltre, possiamo fare in modo che le range()chiamate passino solo attraverso i numeri dispari, tagliando di nuovo il lavoro all'incirca della metà.

http://en.wikipedia.org/wiki/Memoization

http://docs.python.org/dev/library/functools.html

Ciò richiede Python 3.2 o più recente per ottenere lru_cache, ma potrebbe funzionare con un Python precedente se installi una ricetta Python che fornisce lru_cache. Se stai usando Python 2.x dovresti davvero usare xrange()invece di range().

http://code.activestate.com/recipes/577479-simple-caching-decorator/

from functools import lru_cache
import time as time_

@lru_cache()
def is_prime(n):
    return n%2 and all(n%i for i in range(3, n, 2))

def primes_below(x):
    return [(i-6, i) for i in range(9, x+1, 2) if is_prime(i) and is_prime(i-6)]

correct100 = [(5, 11), (7, 13), (11, 17), (13, 19), (17, 23), (23, 29),
        (31, 37), (37, 43), (41, 47), (47, 53), (53, 59), (61, 67), (67, 73),
        (73, 79), (83, 89)]
assert(primes_below(100) == correct100)

a = time_.time()
print(primes_below(30*1000))
b = time_.time()

elapsed = b - a
print("{} msec".format(round(elapsed * 1000)))

Quanto sopra ha richiesto solo un tempo molto breve per la modifica. Ho deciso di fare un ulteriore passo avanti e fare in modo che il test dei numeri primi provi solo i divisori primi e solo fino alla radice quadrata del numero da testare. Il modo in cui l'ho fatto funziona solo se controlli i numeri in ordine, quindi può accumulare tutti i numeri primi man mano che procede; ma questo problema stava già controllando i numeri in ordine, quindi andava bene.

Sul mio laptop (niente di speciale; il processore è un AMD Turion II "K625" da 1,5 GHz) questa versione ha prodotto una risposta per 100K in meno di 8 secondi.

from functools import lru_cache
import math
import time as time_

known_primes = set([2, 3, 5, 7])

@lru_cache(maxsize=128)
def is_prime(n):
    last = math.ceil(math.sqrt(n))
    flag = n%2 and all(n%x for x in known_primes if x <= last)
    if flag:
        known_primes.add(n)
    return flag

def primes_below(x):
    return [(i-6, i) for i in range(9, x+1, 2) if is_prime(i) and is_prime(i-6)]

correct100 = [(5, 11), (7, 13), (11, 17), (13, 19), (17, 23), (23, 29),
        (31, 37), (37, 43), (41, 47), (47, 53), (53, 59), (61, 67), (67, 73),
        (73, 79), (83, 89)]
assert(primes_below(100) == correct100)

a = time_.time()
print(primes_below(100*1000))
b = time_.time()

elapsed = b - a
print("{} msec".format(round(elapsed * 1000)))

Il codice sopra è abbastanza facile da scrivere in Python, Ruby, ecc. Ma sarebbe più un problema in C.

Non è possibile confrontare i numeri di questa versione con i numeri delle altre versioni senza riscrivere le altre per utilizzare trucchi simili. Non sto cercando di dimostrare nulla qui; Pensavo solo che il problema fosse divertente e volevo vedere che tipo di facili miglioramenti delle prestazioni potevo raccogliere.


lru_cacheè decisamente elegante. Per alcune classi di problemi, come la generazione di numeri di Fibonacci successivi, può dare un'enorme velocità semplicemente aggiungendo quel decoratore di una riga alla funzione! Ecco un link a un discorso di Raymond Hettinger che copre lru_cachecirca 26 minuti. Blip.tv/pycon-us-videos-2009-2010-2011/…
steveha

3
Usando lru_cache, in realtà usi un altro algoritmo invece del codice grezzo. Quindi le prestazioni riguardano l'algoritmo ma non il linguaggio stesso.
Eastsun

1
@ Eastsun - Non capisco cosa intendi. lru_cacheevita di ripetere un calcolo già fatto di recente, e basta; Non vedo come questo sia "effettivamente noi [ing] un altro algoritmo". E Python soffre di essere lento, ma trae vantaggio dall'avere cose interessanti come lru_cache; Non vedo niente di sbagliato nell'usare le parti benefiche di una lingua. E ho detto che non si dovrebbe confrontare il tempo di esecuzione della mia risposta con le altre lingue senza apportare modifiche simili alle altre. Quindi, non capisco cosa intendi.
steveha

@Eastsun ha ragione, ma d'altra parte dovrebbe essere consentita la comodità di un linguaggio di livello superiore a meno che non vengano forniti ulteriori vincoli. lru_cache sacrificherà la memoria per la velocità e aggiusterà la complessità algoritmica.
Matt Joiner

2
se usi un altro algoritmo potresti provare Sieve of Eratosthenes. La versione di Python ha prodotto una risposta per 100K in meno di 0.03secondi ( 30ms) .
jfs

7

Non dimenticare Fortran! (Per lo più scherzando, ma mi aspetterei prestazioni simili a C). Le affermazioni con punti esclamativi sono facoltative, ma di buon stile. ( !è un personaggio di commento in fortran 90)

logical function isprime(n)
IMPLICIT NONE !
integer :: n,i
do i=2,n
   if(mod(n,i).eq.0)) return .false.
enddo
return .true.
end

subroutine findprimes(m)
IMPLICIT NONE !
integer :: m,i
logical, external :: isprime

do i=11,m
   if(isprime(i) .and. isprime(i-6))then
      write(*,*) i-6,i
   endif
enddo
end

program main
findprimes(10*1000)
end

6

Non ho potuto resistere a fare alcune delle ottimizzazioni più ovvie per la versione C che ha reso il test 100k ora prendere 0.3s sulla mia macchina (5 volte più veloce della versione C nella domanda, entrambe compilate con MSVC 2010 / Ox) .

int isprime( int x )
{
    int i, n;
    for( i = 3, n = x >> 1; i <= n; i += 2 )
        if( x % i == 0 )
            return 0;
    return 1;
}

void findprimes( int m )
{
    int i, s = 3; // s is bitmask of primes in last 3 odd numbers
    for( i = 11; i < m; i += 2, s >>= 1 ) {
        if( isprime( i ) ) {
            if( s & 1 )
                printf( "%d %d\n", i - 6, i );
            s |= 1 << 3;
        }
    }
}

main() {
    findprimes( 10 * 1000 );
}

Ecco l'implementazione identica in Java:

public class prime
{
    private static boolean isprime( final int x )
    {
        for( int i = 3, n = x >> 1; i <= n; i += 2 )
            if( x % i == 0 )
                return false;
        return true;
    }

    private static void findprimes( final int m )
    {
        int s = 3; // s is bitmask of primes in last 3 odd numbers
        for( int i = 11; i < m; i += 2, s >>= 1 ) {
            if( isprime( i ) ) {
                if( ( s & 1 ) != 0 )
                    print( i );
                s |= 1 << 3;
            }
        }
    }

    private static void print( int i )
    {
        System.out.println( ( i - 6 ) + " " + i );
    }

    public static void main( String[] args )
    {
        // findprimes( 300 * 1000 ); // for some JIT training
        long time = System.nanoTime();
        findprimes( 10 * 1000 );
        time = System.nanoTime() - time;
        System.err.println( "time: " + ( time / 10000 ) / 100.0 + "ms" );
    }
}

Con Java 1.7.0_04 funziona quasi esattamente alla stessa velocità della versione C. La VM del client o del server non mostra molta differenza, tranne che la formazione JIT sembra aiutare un po 'la VM del server (~ 3%) mentre non ha quasi alcun effetto con la VM del client. L'output in Java sembra essere più lento che in C. Se l'output viene sostituito con un contatore statico in entrambe le versioni, la versione Java viene eseguita un po 'più velocemente della versione C.

Questi sono i miei tempi per la corsa 100k:

  • 319ms C compilato con / Ox e output su> NIL:
  • 312 ms C compilato con / Ox e contatore statico
  • VM client Java da 324 ms con output su> NIL:
  • VM client Java da 299 ms con contatore statico

e la corsa 1M (16386 risultati):

  • 24.95s C compilato con / Ox e contatore statico
  • 25.08s VM client Java con contatore statico
  • VM del server Java 24.86s con contatore statico

Anche se questo non risponde davvero alle tue domande, mostra che piccoli aggiustamenti possono avere un impatto notevole sulle prestazioni. Quindi, per poter confrontare realmente le lingue dovresti cercare di evitare il più possibile tutte le differenze algoritmiche.

Dà anche un indizio sul perché Scala sembra piuttosto veloce. Funziona su Java VM e quindi beneficia delle sue prestazioni impressionanti.


1
È più veloce andare a sqrt (x) invece di x >> 1 per la funzione prime check.
Eve Freeman

4

In Scala prova a usare Tuple2 invece di List, dovrebbe andare più veloce. Basta rimuovere la parola "List" poiché (x, y) è una Tuple2.

Tuple2 è specializzato per Int, Long e Double, il che significa che non dovrà box / unbox quei tipi di dati non elaborati. Sorgente Tuple2 . L'elenco non è specializzato. Fonte elenco .


Allora non puoi chiamarlo forall. Ho anche pensato che questo potrebbe non essere il codice più efficiente (più perché una grande raccolta rigorosa viene creata per grandi ninvece di usare solo una vista), ma è certamente breve + elegante, e sono rimasto sorpreso di quanto bene abbia funzionato nonostante l'utilizzo di un molto stile funzionale.
0__

Hai ragione, pensavo che "forAll" fosse lì. Tuttavia dovrebbe esserci un grande miglioramento rispetto a List e non sarebbe così male avere quelle 2 chiamate.
Tomas Lazaro

2
è davvero più veloce, con def sexyPrimes(n: Int) = (11 to n).map(i => (i-6, i)).filter({ case (i, j) => isPrime(i) && isPrime(j) })esso è circa il 60% più veloce qui, quindi dovrebbe battere il codice C :)
0__

Hmm, ottengo solo un aumento delle prestazioni del 4 o 5%
Luigi Plinge

1
Trovo collectsostanzialmente più lento. Più veloce è se fai prima il filtro e poi la mappa. withFilterè leggermente più veloce perché in realtà non crea raccolte intermedie. (11 to n) withFilter (i => isPrime(i - 6) && isPrime(i)) map (i => (i - 6, i))
Luigi Plinge

4

Ecco il codice per la versione Go (golang.org):

package main

import (
    "fmt"
)


func main(){
    findprimes(10*1000)
}

func isprime(x int) bool {
    for i := 2; i < x; i++ {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func findprimes(m int){
    for i := 11; i < m; i++ {
        if isprime(i) && isprime(i-6) {
            fmt.Printf("%d %d\n", i-6, i)
        }
    }
}

Funzionava alla stessa velocità della versione C.

Utilizzando un Asus u81a Intel Core 2 Duo T6500 2.1GHz, cache L2 da 2MB, FSB 800MHz. 4 GB di RAM

La versione 100k: C: 2.723s Go: 2.743s

Con 1000000 (1 M invece di 100 K): C: 3m35.458s Go: 3m36.259s

Ma penso che sarebbe giusto utilizzare le funzionalità di multithreading integrate di Go e confrontare quella versione con la normale versione C (senza multithreading), solo perché è quasi troppo facile eseguire il multithreading con Go.

Aggiornamento: ho fatto una versione parallela usando Goroutines in Go:

package main

import (
  "fmt"
  "runtime"
)

func main(){
    runtime.GOMAXPROCS(4)
    printer := make(chan string)
    printer2 := make(chan string)
    printer3 := make(chan string)
    printer4 := make(chan string)
    finished := make(chan int)

    var buffer, buffer2, buffer3 string

    running := 4
    go findprimes(11, 30000, printer, finished)
    go findprimes(30001, 60000, printer2, finished)
    go findprimes(60001, 85000, printer3, finished)
    go findprimes(85001, 100000, printer4, finished)

    for {
      select {
        case i := <-printer:
          // batch of sexy primes received from printer channel 1, print them
          fmt.Printf(i)
        case i := <-printer2:
          // sexy prime list received from channel, store it
          buffer = i
        case i := <-printer3:
          // sexy prime list received from channel, store it
          buffer2 = i
        case i := <-printer4:
          // sexy prime list received from channel, store it
          buffer3 = i
        case <-finished:
          running--
          if running == 0 {
              // all goroutines ended
              // dump buffer to stdout
              fmt.Printf(buffer)
              fmt.Printf(buffer2)
              fmt.Printf(buffer3)
              return
          }
      }
    }
}

func isprime(x int) bool {
    for i := 2; i < x; i++ {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func findprimes(from int, to int, printer chan string, finished chan int){
    str := ""
    for i := from; i <= to; i++ {
        if isprime(i) && isprime(i-6) {
            str = str + fmt.Sprintf("%d %d\n", i-6, i)
      }
    }
    printer <- str
    //fmt.Printf("Finished %d to %d\n", from, to)
    finished <- 1
}

La versione parallelizzata utilizzata in media 2,743 secondi, esattamente lo stesso tempo utilizzato dalla versione normale.

La versione parallelizzata è stata completata in 1.706 secondi. Utilizzava meno di 1,5 Mb di RAM.

Una cosa strana: il mio dual core kubuntu 64bit non ha mai raggiunto il picco in entrambi i core. Sembrava che Go usasse un solo core. Risolto con una chiamata aruntime.GOMAXPROCS(4)

Aggiornamento: ho eseguito la versione paralellizzata fino a 1 milione di numeri. Uno dei core della mia CPU era sempre al 100%, mentre l'altro non era affatto utilizzato (strano). Ci è voluto un minuto intero in più rispetto alle versioni C e Go regolari. :(

Con 1000000 (1 M invece di 100 K):

C: 3m35.458s Go: 3m36.259s Go using goroutines:3 m 27,137 s2m16.125s

La versione 100k:

C: 2.723s Go: 2.743s Go using goroutines: 1.706s


Quanti core hai usato btw?
om-nom-nom

2
Ho un Asus u81a Intel Core 2 Duo T6500 2.1GHz, cache L2 da 2MB, FSB 800MHz. 4GB RAM
Sebastián Grignoli

Hai effettivamente compilato la versione C con le ottimizzazioni abilitate? Il compilatore Go predefinito non è in linea e di solito subirà un enorme calo delle prestazioni rispetto al C ottimizzato in questi tipi di confronti. Aggiungi -O3o meglio.
Matt Joiner

L'ho appena fatto, non prima, e la versione 100K ha richiesto lo stesso tempo con o senza -O3
Sebastián Grignoli

Stessa cosa per la versione 1M. Forse queste particolari operazioni (stiamo testando un sottoinsieme molto piccolo) sono ben ottimizzate per impostazione predefinita.
Sebastián Grignoli

4

Solo per il gusto di farlo, ecco una versione parallela di Ruby.

require 'benchmark'

num = ARGV[0].to_i

def is_prime?(n)
  (2...n).all?{|m| n%m != 0 }
end

def sexy_primes_default(x)
    (9..x).map do |i|
        [i-6, i]
    end.select do |j|
        j.all?{|j| is_prime? j}
    end
end

def sexy_primes_threads(x)
    partition = (9..x).map do |i|
        [i-6, i]
    end.group_by do |x|
        x[0].to_s[-1]
    end
    threads = Array.new
    partition.each_key do |k|
       threads << Thread.new do
            partition[k].select do |j|
                j.all?{|j| is_prime? j}
            end
        end
    end
    threads.each {|t| t.join}
    threads.map{|t| t.value}.reject{|x| x.empty?}
end

puts "Running up to num #{num}"

Benchmark.bm(10) do |x|
    x.report("default") {a = sexy_primes_default(num)}
    x.report("threads") {a = sexy_primes_threads(num)}
end

Sul mio MacBook Air Core i5 da 1,8 GHz, i risultati in termini di prestazioni sono:

# Ruby 1.9.3
$ ./sexyprimes.rb 100000
Running up to num 100000
                 user     system      total        real
default     68.840000   0.060000  68.900000 ( 68.922703)
threads     71.730000   0.090000  71.820000 ( 71.847346)

# JRuby 1.6.7.2 on JVM 1.7.0_05
$ jruby --1.9 --server sexyprimes.rb 100000
Running up to num 100000
                user     system      total        real
default    56.709000   0.000000  56.709000 ( 56.708000)
threads    36.396000   0.000000  36.396000 ( 36.396000)

# JRuby 1.7.0.preview1 on JVM 1.7.0_05
$ jruby --server sexyprimes.rb 100000
Running up to num 100000
             user     system      total        real
default     52.640000   0.270000  52.910000 ( 51.393000)
threads    105.700000   0.290000 105.990000 ( 30.298000)

Sembra che il JIT della JVM stia dando a Ruby un bel miglioramento delle prestazioni nel caso predefinito, mentre il vero multithreading aiuta JRuby a eseguire il 50% più velocemente nel caso con thread. La cosa più interessante è che JRuby 1.7 migliora il punteggio di JRuby 1.6 di un sano 17%!


3

Sulla base della risposta di x4u , ho scritto una versione scala usando la ricorsione e l'ho migliorata andando solo su sqrt invece di x / 2 per la funzione di controllo prime. Ottengo ~ 250 ms per 100k e ~ 600 ms per 1M. Sono andato avanti e sono andato a 10 milioni in ~ 6 secondi.

import scala.annotation.tailrec

var count = 0;
def print(i:Int) = {
  println((i - 6) + " " + i)
  count += 1
}

@tailrec def isPrime(n:Int, i:Int = 3):Boolean = {
  if(n % i == 0) return false;
  else if(i * i > n) return true;
  else isPrime(n = n, i = i + 2)
}      

@tailrec def findPrimes(max:Int, bitMask:Int = 3, i:Int = 11):Unit = {
  if (isPrime(i)) {
    if((bitMask & 1) != 0) print(i)
    if(i + 2 < max) findPrimes(max = max, bitMask = (bitMask | (1 << 3)) >> 1, i = i + 2)
  } else if(i + 2 < max) {
    findPrimes(max = max, bitMask = bitMask >> 1, i = i + 2)
  }
}

val a = System.currentTimeMillis()
findPrimes(max=10000000)
println(count)
val b = System.currentTimeMillis()
println((b - a).toString + " mils")

Sono anche tornato indietro e ho scritto una versione CoffeeScript (V8 JavaScript), che ottiene ~ 15ms per 100k, 250ms per 1M e 6s per 10M, utilizzando un contatore (ignorando l'I / O). Se accendo l'uscita, ci vogliono ~ 150 ms per 100k, 1s per 1M e 12s per 10M. Non è stato possibile utilizzare la ricorsione della coda qui, sfortunatamente, quindi ho dovuto riconvertirla in loop.

count = 0;
print = (i) ->
  console.log("#{i - 6} #{i}")
  count += 1
  return

isPrime = (n) ->
  i = 3
  while i * i < n
    if n % i == 0
      return false
    i += 2
  return true

findPrimes = (max) ->
  bitMask = 3
  for i in [11..max] by 2
    prime = isPrime(i)
    if prime
      if (bitMask & 1) != 0
        print(i)
      bitMask |= (1 << 3)
    bitMask >>= 1
  return

a = new Date()
findPrimes(1000000)
console.log(count)
b = new Date()
console.log((b - a) + " ms")

2

La risposta alla tua domanda n. 1 è che Sì, la JVM è incredibilmente veloce e sì, la digitazione statica aiuta.

La JVM dovrebbe essere più veloce di C a lungo termine, forse anche più veloce del linguaggio assembly "Normale" - Ovviamente puoi sempre ottimizzare manualmente l'assembly per battere qualsiasi cosa eseguendo la profilazione manuale del runtime e creando una versione separata per ogni CPU, devi solo devono essere sorprendentemente bravi e informati.

Le ragioni della velocità di Java sono:

La JVM può analizzare il codice durante l'esecuzione e ottimizzarlo manualmente, ad esempio se si dispone di un metodo che può essere analizzato staticamente in fase di compilazione per essere una vera funzione e la JVM ha notato che lo si chiamava spesso con lo stesso parametri, POTREBBE effettivamente eliminare completamente la chiamata e iniettare i risultati dell'ultima chiamata (non sono sicuro che Java lo faccia esattamente, ma fa molte cose come questa).

A causa della digitazione statica, la JVM può sapere molto sul tuo codice in fase di compilazione, questo consente di pre-ottimizzare un bel po 'di cose. Consente inoltre al compilatore di ottimizzare individualmente ogni classe senza sapere come un'altra classe intende utilizzarla. Inoltre Java non ha puntatori arbitrari alla posizione di memoria, sa quali valori in memoria possono e non possono essere modificati e può ottimizzare di conseguenza.

L'allocazione dell'heap è MOLTO più efficiente di C, l'allocazione dell'heap di Java è più simile all'allocazione dello stack di C in termini di velocità, ma più versatile. È passato molto tempo nei diversi algoritmi usati qui, è un'arte - per esempio, tutti gli oggetti con una vita breve (come le variabili dello stack di C) sono assegnati a una posizione libera "nota" (nessuna ricerca di un punto libero con abbastanza spazio) e vengono tutti liberati insieme in un unico passaggio (come uno stack pop).

La JVM può conoscere stranezze sull'architettura della CPU e generare codice macchina specifico per una determinata CPU.

La JVM può velocizzare il tuo codice molto tempo dopo che lo hai spedito. Proprio come spostare un programma su una nuova CPU può velocizzarlo, spostarlo su una nuova versione della JVM può anche darti enormi prestazioni di velocità adattate a CPU che non esistevano nemmeno quando hai inizialmente compilato il tuo codice, qualcosa che fisicamente non può fare a meno di una ricetta.

A proposito, la maggior parte della cattiva reputazione per la velocità di java deriva dal lungo tempo di avvio per caricare la JVM (un giorno qualcuno costruirà la JVM nel sistema operativo e questo andrà via!) E dal fatto che molti sviluppatori sono davvero pessimi nello scrivere Codice della GUI (in particolare con thread) che causava spesso la mancata risposta e glitch delle GUI Java. Linguaggi semplici da usare come Java e VB hanno i loro difetti amplificati dal fatto che le capacità del programmatore medio tendono ad essere inferiori rispetto ai linguaggi più complicati.


Dire che l'allocazione dell'heap di JVM è molto più efficiente di C non è sensato, dato che JVM è scritto in C ++.
Daniel C. Sobral

5
@ Il linguaggio DanielC.Sobral non è importante quanto l'implementazione: il codice di implementazione "Heap" di Java non ha niente a che vedere con il C. Java è un sistema multistadio sostituibile altamente ottimizzabile per vari obiettivi con molti anni uomo di impegno nella ricerca, comprese tecniche all'avanguardia sviluppate oggi, C utilizza un mucchio: una semplice struttura di dati sviluppata secoli fa. Il sistema Java è impossibile da implementare per C dato che C consente i puntatori, quindi non può mai garantire spostamenti "sicuri" di blocchi di memoria allocati arbitrariamente senza modifiche al linguaggio (rendendolo non più C)
Bill K

La sicurezza è irrilevante: non hai affermato che fosse più sicuro , hai affermato che era più efficiente . Inoltre, la descrizione nel commento di come funziona C "heap" non ha alcuna relazione con la realtà.
Daniel C. Sobral

Devi aver frainteso il mio significato di "Sicuro": Java è in grado di spostare un blocco arbitrario di memoria in qualsiasi momento perché sa che può farlo, C non è in grado di ottimizzare la posizione della memoria perché potrebbe esserci un puntatore che potrebbe farvi riferimento. Anche l'heap AC viene solitamente implementato come un heap che è una struttura dati. Gli heap C ++ venivano implementati con strutture di heap come lo era C (da cui il nome, "Heap") Non ho controllato in C ++ per alcuni anni, quindi potrebbe non essere più vero, ma è ancora limitato dal non essere in grado di riorganizza a piacimento piccoli pezzi di memoria allocata dall'utente.
Bill K
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.