2011-02-10 18 views
13

Un estratto dal libro scalinata:Le classi astratte in Scala hanno prestazioni migliori dei tratti?

Se l'efficienza è molto importante, magra verso l'utilizzo di una classe. La maggior parte dei runtime Java rendono una chiamata al metodo virtuale di un membro della classe un'operazione più veloce rispetto a un richiamo del metodo di interfaccia. I tratti vengono compilati per le interfacce e quindi possono pagare un leggero sovraccarico di prestazioni. Tuttavia, dovresti scegliere solo se sai che il tratto in questione costituisce un collo di bottiglia delle prestazioni e hai la prova che l'utilizzo di una classe invece in realtà risolve il problema.

Ho scritto un codice semplice per vedere cosa succede veramente dietro le quinte. E ho notato che invokevirtual viene utilizzato in caso di una classe astratta e invokeinterface in caso di un'interfaccia. Ma a prescindere dal tipo di codice che ho scritto, eseguivano sempre in modo approssimativo lo stesso. Io uso HotSpot 1.6.0_18 in modalità server.

JIT sta facendo un ottimo lavoro di ottimizzazione? Qualcuno ha un codice di esempio che dimostra che il reclamo dal libro su invokevirutal è l'operazione più veloce?

+0

Se stai pensando seriamente a questo tipo di ottimizzazione a basso livello, stai usando un linguaggio di programmazione sbagliato. – Raphael

+4

@Raphael Se stai usando Scala su un sistema Android o simili potresti aver bisogno di prestare attenzione a tutte quelle cose alle quali normalmente non penserai due volte. – wheaties

+4

Non sto cercando di ottimizzare nulla. È più di un esercizio di apprendimento e soddisfare la mia curiosità. Apprezzo comunque il tuo commento @Raphael. –

risposta

7

Se HotSpot rileva che tutte le istanze nel sito di chiamata sono dello stesso tipo, è in grado di utilizzare una chiamata di metodo monomorfica e entrambi i metodi virtuali e di interfaccia sono ottimizzati allo stesso modo. I documenti PerformanceTechniques e VirtualCalls non fanno distinzione tra metodi virtuali e di interfaccia.

Ma nel caso generale non monomorfico potrebbe esserci qualche differenza. Il documento InterfaceCalls dice:

Non c'è semplice schema prefisso di in cui vengono visualizzati i metodi di un'interfaccia a offset fisse all'interno di ogni classe che implementa l'interfaccia. Invece, nel caso generale (non monomorfico), una routine di stub codificata in assembly deve recuperare una lista di interfacce implementate dal klassOop del ricevitore, e camminare nell'elenco che cerca l'interfaccia di destinazione corrente.

Inoltre conferma che il caso monomorfa è la stessa per entrambi:

Quasi le stesse ottimizzazioni applicano per interfacciarsi chiamate come alle chiamate virtuali. Come per le chiamate virtuali, la maggior parte delle chiamate di interfaccia sono monomorfiche e possono quindi essere visualizzate come chiamate dirette con un controllo economico.

Altre JVM potrebbero avere diverse ottimizzazioni.

È possibile provare un micro benchmark (if you know how) che chiama i metodi su più classi che implementano la stessa interfaccia e su più classi che estendono la stessa classe astratta. In questo modo dovrebbe essere possibile forzare la JVM a utilizzare chiamate di metodo non monomorfiche. (Anche se nella vita reale qualsiasi differenza potrebbe non avere importanza, poiché la maggior parte dei siti di chiamate è comunque monomorfica.)

0

Una citazione da Inside the Java Virtual Machine(Istruzioni invocazione e velocità):

quando la macchina virtuale Java incontra un invokevirtual istruzioni e risolve il simbolico riferimento a un riferimento diretto ad un Metodo di istanza , che il riferimento diretto è probabilmente un offset in una tabella di metodo . Da quel momento in poi, è possibile utilizzare lo stesso offset . Per un'istruzione invokeinterface, tuttavia, la macchina virtuale dovrà ricerca attraverso la tabella metodo ogni singolo volta che l'istruzione è incontra, perché non può assumere l'offset è lo stesso della volta precedente .

+0

Sì, l'ho letto e ho capito perché potrebbe essere più lento, ma mi piacerebbe ancora vedere prove in pratica. –

+0

Sono in dubbio che qualcuno sacrificherà abbastanza tempo per creare un benchmark serio per provare cose ovvie :) –

+5

* All'interno della Java Virtual Machine * è stato scritto oltre 10 anni fa. Non è al passo con le ottimizzazioni delle JVM attuali. –

3

La linea di fondo è che dovrai misurarlo tu stesso per la tua applicazione per vedere se è importante . È possibile ottenere risultati piuttosto controintuitivi con l'attuale JVM. Prova questo.

File TraitAbstractPackage.scala

package traitvsabstract 

trait T1 { def x: Int; def inc: Unit } 
trait T2 extends T1 { def x_=(x0: Int): Unit } 
trait T3 extends T2 { def inc { x = x + 1 } } 

abstract class C1 { def x: Int; def inc: Unit } 
abstract class C2 extends C1 { def x_=(x0: Int): Unit } 
abstract class C3 extends C2 { def inc { x = x + 1 } } 

TraitVsAbstract.scala File

object TraitVsAbstract { 
    import traitvsabstract._ 

    class Ta extends T3 { var x: Int = 0} 
    class Tb extends T3 { 
    private[this] var y: Long = 0 
    def x = y.toInt 
    def x_=(x0: Int) { y = x0 } 
    } 
    class Tc extends T3 { 
    private[this] var xHidden: Int = 0 
    def x = xHidden 
    def x_=(x0: Int) { if (x0 > xHidden) xHidden = x0 } 
    } 

    class Ca extends C3 { var x: Int = 0 } 
    class Cb extends C3 { 
    private[this] var y: Long = 0 
    def x = y.toInt 
    def x_=(x0: Int) { y = x0 } 
    } 
    class Cc extends C3 { 
    private[this] var xHidden: Int = 0 
    def x = xHidden 
    def x_=(x0: Int) { if (x0 > xHidden) xHidden = x0 } 
    } 

    def Tbillion3(t: T3) = { 
    var i=0; while (i<1000000000) { t.inc; i+=1 }; t.x 
    } 

    def Tbillion1(t: T1) = { 
    var i=0; while (i<1000000000) { t.inc; i+=1 }; t.x 
    } 

    def Cbillion3(c: C3) = { 
    var i=0; while (i<1000000000) { c.inc; i+=1 }; c.x 
    } 

    def Cbillion1(c: C1) = { 
    var i=0; while (i<1000000000) { c.inc; i+=1 }; c.x 
    } 

    def ptime(f: => Int) { 
    val t0 = System.nanoTime 
    val ans = f.toString 
    val t1 = System.nanoTime 
    printf("Answer: %s; elapsed: %.2f seconds\n",ans,(t1-t0)*1e-9) 
    } 

    def main(args: Array[String]) { 
    for (i <- 1 to 3) { 
     println("Iteration "+i) 
     val t1s,t3s = List(new Ta, new Tb, new Tc) 
     val c1s,c3s = List(new Ca, new Cb, new Cc) 
     t1s.foreach(x => ptime(Tbillion1(x))) 
     t3s.foreach(x => ptime(Tbillion3(x))) 
     c1s.foreach(x => ptime(Cbillion1(x))) 
     c3s.foreach(x => ptime(Cbillion3(x))) 
     println 
    } 
    } 
} 

Ognuno dovrebbe stampare 1000000000 come la risposta, e il tempo impiegato dovrebbe essere pari a zero (se la JVM è davvero intelligente) o circa il tempo necessario per aggiungere un miliardo di numeri. Ma almeno sul mio sistema, Sun JVM ottimizza al contrario - le corse ripetute si rallentano - e le classi astratte sono più lente dei tratti. (Si potrebbe voler correre con java -XX:+PrintCompilation per cercare di capire cosa va storto, ho il sospetto di zombi.)

Inoltre, vale la pena notare che scalac -optimise non fa nulla per migliorare le cose: dipende interamente dalla JVM.

La JVM JRockit al contrario si trasforma in una prestazione mediocre, ma ancora una volta, i tratti battono le classi. Dal momento che i tempi sono coerenti, li segnalerò: 3.35 per le classi (3.62s per quella con un'istruzione if) contro 2.51 secondi per tutti i tratti, if-statement o no.

(Trovo che questa tendenza sia generalmente vera: Hotspot produce prestazioni veloci e brillanti in alcuni casi, mentre in altri (come in questo caso) si confonde ed è tremendamente lento, JRockit non è mai super veloce - non disturbare cercando di ottenere prestazioni simili a C anche da primitive, ma raramente errori grossolani.)

Problemi correlati