Prestazioni di Scala rispetto a Java

41

Prima di tutto vorrei chiarire che questa non è una domanda di lingua X rispetto alla lingua Y per determinare quale è meglio.

Utilizzo Java da molto tempo e intendo continuare a utilizzarlo. Parallelamente a questo, attualmente sto imparando Scala con grande interesse: a parte le piccole cose che prendono un po 'd'abitudine alla mia impressione è che posso davvero lavorare molto bene in questa lingua.

La mia domanda è: in che modo il software scritto in Scala si confronta con il software scritto in Java in termini di velocità di esecuzione e consumo di memoria? Ovviamente, questa è una domanda difficile a cui rispondere in generale, ma mi aspetterei che i costrutti di livello superiore come la corrispondenza dei pattern, le funzioni di ordine superiore e così via, introducano alcuni overhead.

Tuttavia, la mia attuale esperienza in Scala è limitata a piccoli esempi con meno di 50 righe di codice e non ho ancora eseguito alcun benchmark fino ad ora. Quindi, non ho dati reali.

Se risulta che Scala ha un sovraccarico su Java, ha senso avere progetti Scala / Java misti, dove si codifica le parti più complesse in Scala e le parti critiche per le prestazioni in Java? È una pratica comune?

EDIT 1

Ho eseguito un piccolo benchmark: creare un elenco di numeri interi, moltiplicare ogni numero intero per due e inserirlo in un nuovo elenco, stampare l'elenco risultante. Ho scritto un'implementazione Java (Java 6) e un'implementazione Scala (Scala 2.9). Ho eseguito entrambi su Eclipse Indigo sotto Ubuntu 10.04.

I risultati sono comparabili: 480 ms per Java e 493 ms per Scala (in media oltre 100 iterazioni). Ecco i frammenti che ho usato.

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

Quindi, in questo caso sembra che il sovraccarico di Scala (usando range, map, lambda) sia veramente minimo, che non è lontano dalle informazioni fornite da Ingegnere mondiale.

Forse ci sono altri costrutti di Scala che dovrebbero essere usati con attenzione perché sono particolarmente pesanti da eseguire?

EDIT 2

Alcuni di voi hanno sottolineato che le stampe nei loop interni occupano la maggior parte di esse il tempo di esecuzione. Li ho rimossi e ho impostato la dimensione degli elenchi su 100000 anziché su 20000. La media risultante era di 88 ms per Java e 49 ms per Scala.

    
posta Giorgio 24.01.2012 - 19:47
fonte

4 risposte

39

C'è una cosa che puoi fare in modo conciso ed efficiente in Java che non puoi usare in Scala: enumerazioni. Per tutto il resto, anche per i costrutti che sono lenti nella libreria di Scala, è possibile ottenere versioni efficienti che funzionano in Scala.

Quindi, per la maggior parte, non è necessario aggiungere Java al codice. Anche per il codice che utilizza l'enumerazione in Java, c'è spesso una soluzione in Scala che è adeguata o buona - io pongo l'eccezione sulle enumerazioni che hanno metodi aggiuntivi e i cui valori costanti int sono usati.

Per quanto riguarda ciò a cui prestare attenzione, ecco alcune cose.

  • Se usi il modello della mia biblioteca arricchire, converti sempre in una classe. Ad esempio:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
    
  • Fai attenzione ai metodi di raccolta: poiché sono per lo più polimorfici, JVM non li ottimizza. Non è necessario evitarli, ma prestare attenzione ad essi in sezioni critiche. Tieni presente che for in Scala viene implementato tramite chiamate ai metodi e classi anonime.

  • Se si utilizza una classe Java, come String , Array o AnyVal classi corrispondenti a primitive Java, si preferiscono i metodi forniti da Java quando esistono alternative. Ad esempio, utilizza length su String e Array anziché size .

  • Evita l'uso incauto delle conversioni implicite, dato che puoi ritrovarti a utilizzare le conversioni per errore anziché per design.

  • Estendi le classi anziché i tratti. Ad esempio, se estendi Function1 , estendi AbstractFunction1 invece.

  • Usa -optimise e specializzazione per ottenere la maggior parte di Scala.

  • Comprendi cosa sta succedendo: javap è tuo amico, così come lo sono un sacco di bandiere Scala che mostrano cosa sta succedendo.

  • Gli idiomi di Scala sono progettati per migliorare la correttezza e rendere il codice più conciso e mantenibile. Non sono progettati per la velocità, quindi se hai bisogno di usare null invece di Option in un percorso critico, fallo! C'è una ragione perché Scala è multi-paradigma.

  • Ricorda che la vera misura delle prestazioni è il codice in esecuzione. Vedi questa domanda per un esempio di cosa potrebbe accadere se ignori quella regola.

risposta data 24.01.2012 - 23:04
fonte
21

Secondo Benchmark Game per un singolo core, sistema a 32 bit, Scala è a una mediana dell'80% veloce come Java. Le prestazioni sono approssimativamente le stesse per un computer Quad Core x64. Anche l'utilizzo della memoria e la densità del codice sono molto simili in la maggior parte dei casi. Direi basandosi su queste analisi (piuttosto non scientifiche) che sei corretto nell'affermare che Scala aggiunge un po 'di overhead a Java. Non sembra aggiungere tonnellate di spese generali, quindi sospetto che la diagnosi di articoli di ordine superiore che richiedono più spazio / tempo sia la più corretta.

    
risposta data 24.01.2012 - 20:21
fonte
18
  • Le prestazioni di Scala sono molto buone se si scrive codice in Java / C in Scala. Il compilatore userà le primitive JVM per Int , Char , ecc. Quando può. Sebbene i loop siano altrettanto efficaci in Scala.
  • Tieni presente che le espressioni lambda sono compilate in istanze di sottoclassi anonime delle classi Function . Se si passa un lambda a map , la classe anonima deve essere istanziata (e alcuni locali possono aver bisogno di essere passati), e poi ogni iterazione ha overhead di chiamata di funzione extra (con qualche parametro che passa) dalle chiamate apply .
  • Molte classi come scala.util.Random sono solo wrapper attorno a classi JRE equivalenti. La chiamata alla funzione extra è un po 'dispendiosa.
  • Fai attenzione agli impliciti nel codice critico delle prestazioni. java.lang.Math.signum(x) è molto più diretto di x.signum() , che converte in RichInt e ritorno.
  • Il principale vantaggio prestazionale di Scala su Java è la specializzazione. Tieni presente che la specializzazione viene utilizzata con parsimonia nel codice della libreria.
risposta data 24.01.2012 - 21:51
fonte
5
  • a) Dalla mia conoscenza limitata, devo sottolineare, che il codice nel metodo statico principale non può essere ottimizzato molto bene. Dovresti spostare il codice critico in una posizione diversa.
  • b) Da lunghe osservazioni consiglierei di non eseguire un output pesante sul test delle prestazioni (eccetto che è esattamente ciò che ti piace ottimizzare, ma chi dovrebbe mai leggere 2 milioni di valori?). Stai misurando println, che non è molto interessante. Sostituzione di println con max:
(1 to 20000).toList.map (_ * 2).max

riduce il tempo da 800 ms a 20 sul mio sistema.

  • c) La comprensione preliminare è nota per essere un po 'lenta (mentre dobbiamo ammettere che sta migliorando sempre). Utilizzare invece le funzioni while o tailrecursive. Non in questo esempio, dove è il ciclo esterno. Usa l'annotazione @ tailrec, per testare la tairecursiveness.
  • d) Il confronto con C / Assembler fallisce. Ad esempio, non si riscrive il codice scala per diverse architetture. Altre importanti differenze rispetto alle situazioni storiche sono
    • Compilatore JIT, che ottimizza al volo e forse in modo dinamico, a seconda dei dati di input
    • L'importanza dei problemi di cache
    • La crescente importanza dell'invocazione parallela. Scala oggi ha soluzioni per lavorare senza molti overhead in parallelo. Questo non è possibile in Java, tranne che fai molto più lavoro.
risposta data 24.01.2012 - 23:15
fonte

Leggi altre domande sui tag