Le chiamate statiche Java sono più o meno costose delle chiamate non statiche?


Risposte:


74

Primo: non dovresti scegliere tra statico e non statico sulla base delle prestazioni.

Secondo: in pratica, non farà alcuna differenza. L'hotspot può scegliere di ottimizzare in modi che rendono le chiamate statiche più veloci per un metodo, le chiamate non statiche più veloci per un altro.

Terzo: gran parte dei miti che circondano statico e non statico si basano su JVM molto vecchie (che non si avvicinavano all'ottimizzazione di Hotspot) o su alcune curiosità ricordate su C ++ (in cui una chiamata dinamica utilizza un accesso in più alla memoria di una chiamata statica).


1
Hai assolutamente ragione che non dovresti preferire metodi statici basati solo su questo. Tuttavia, nel caso in cui i metodi statici si adattino bene al progetto, è utile sapere che sono almeno altrettanto veloci, se non più veloci dei metodi di istanza e non dovrebbero essere esclusi sulla base delle prestazioni.
Il

2
@AaronDigulla -.- e se ti dicessi che sono venuto qui perché sto ottimizzando in questo momento, non prematuramente, ma quando ne ho davvero bisogno? Hai pensato che OP volesse ottimizzare prematuramente, ma sai che questo sito è un po 'globale ... giusto? Non voglio essere scortese, ma per favore non dare per scontato cose del genere la prossima volta.
Dalibor Filus

1
@DaliborFilus ho bisogno di trovare un equilibrio. L'uso di metodi statici causa tutti i tipi di problemi, quindi dovrebbero essere evitati, soprattutto quando non sai cosa stai facendo. In secondo luogo, la maggior parte del codice "lento" è a causa della progettazione (cattiva), non perché il linguaggio di scelta è lento. Se il codice è lento, i metodi statici probabilmente non lo salveranno a meno che i metodi di chiamata che non fanno assolutamente nulla . Nella maggior parte dei casi, il codice nei metodi sminuisce il sovraccarico della chiamata.
Aaron Digulla

6
Downvoted. Questo non risponde alla domanda. La domanda posta sui vantaggi in termini di prestazioni. Non ha chiesto pareri sui principi di progettazione.
Colm Bhandal

4
Se insegnassi a un pappagallo a dire "l'ottimizzazione prematura è la radice di tutti i mali", otterrei 1000 voti da persone che sanno tanto di prestazioni quanto il pappagallo.
rghome

62

Quattro anni dopo...

Ok, nella speranza di risolvere questa domanda una volta per sempre, ho scritto un benchmark che mostra come i diversi tipi di chiamate (virtuali, non virtuali, statiche) si confrontano tra loro.

L'ho eseguito su ideone , e questo è quello che ho ottenuto:

(È meglio un numero maggiore di iterazioni).

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

Come previsto, le chiamate ai metodi virtuali sono le più lente, le chiamate ai metodi non virtuali sono più veloci e le chiamate ai metodi statici sono ancora più veloci.

Quello che non mi aspettavo era che le differenze fossero così pronunciate: le chiamate ai metodi virtuali sono state misurate per essere eseguite a meno della metà della velocità delle chiamate ai metodi non virtuali, che a loro volta sono state misurate per essere eseguite un intero 15% più lentamente delle chiamate statiche. Questo è ciò che mostrano queste misurazioni; le differenze effettive devono infatti essere leggermente più pronunciate, poiché per ogni chiamata al metodo virtuale, non virtuale e statico, il mio codice di benchmarking ha un sovraccarico costante aggiuntivo di incremento di una variabile intera, controllo di una variabile booleana e ciclo se non vero.

Suppongo che i risultati varieranno da CPU a CPU e da JVM a JVM, quindi provalo e guarda cosa ottieni:

import java.io.*;

class StaticVsInstanceBenchmark
{
    public static void main( String[] args ) throws Exception
    {
        StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
        program.run();
    }

    static final int DURATION = 1000;

    public void run() throws Exception
    {
        doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ), 
                     new NonVirtualTest( new ClassWithNonVirtualMethod() ), 
                     new StaticTest() );
    }

    void doBenchmark( Test... tests ) throws Exception
    {
        System.out.println( "  Name          |  Iterations" );
        doBenchmark2( devNull, 1, tests ); //warmup
        doBenchmark2( System.out, DURATION, tests );
        System.out.println( "Done." );
    }

    void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
    {
        for( Test test : tests )
        {
            long iterations = runTest( duration, test );
            printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
        }
    }

    long runTest( int duration, Test test ) throws Exception
    {
        test.terminate = false;
        test.count = 0;
        Thread thread = new Thread( test );
        thread.start();
        Thread.sleep( duration );
        test.terminate = true;
        thread.join();
        return test.count;
    }

    static abstract class Test implements Runnable
    {
        boolean terminate = false;
        long count = 0;
    }

    static class ClassWithStaticStuff
    {
        static int staticDummy;
        static void staticMethod() { staticDummy++; }
    }

    static class StaticTest extends Test
    {
        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                ClassWithStaticStuff.staticMethod();
            }
        }
    }

    static class ClassWithVirtualMethod implements Runnable
    {
        int instanceDummy;
        @Override public void run() { instanceDummy++; }
    }

    static class VirtualTest extends Test
    {
        final Runnable runnable;

        VirtualTest( Runnable runnable )
        {
            this.runnable = runnable;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                runnable.run();
            }
        }
    }

    static class ClassWithNonVirtualMethod
    {
        int instanceDummy;
        final void nonVirtualMethod() { instanceDummy++; }
    }

    static class NonVirtualTest extends Test
    {
        final ClassWithNonVirtualMethod objectWithNonVirtualMethod;

        NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
        {
            this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                objectWithNonVirtualMethod.nonVirtualMethod();
            }
        }
    }

    static final PrintStream devNull = new PrintStream( new OutputStream() 
    {
        public void write(int b) {}
    } );
}

Vale la pena notare che questa differenza di prestazioni è applicabile solo al codice che non fa altro che invocare metodi senza parametri. Qualunque altro codice tu abbia tra le invocazioni diluirà le differenze e questo include il passaggio di parametri. In realtà, la differenza del 15% tra chiamate statiche e non virtuali è probabilmente spiegata completamente dal fatto che il thispuntatore non deve essere passato al metodo statico. Quindi, ci vorrebbe solo una piccola quantità di codice che fa cose banali tra le chiamate per diluire la differenza tra i diversi tipi di chiamate al punto da non avere alcun impatto netto.

Inoltre, le chiamate ai metodi virtuali esistono per un motivo; hanno uno scopo da servire e sono implementati utilizzando i mezzi più efficienti forniti dall'hardware sottostante. (Il set di istruzioni della CPU.) Se, nel tuo desiderio di eliminarli sostituendoli con chiamate non virtuali o statiche, finisci per dover aggiungere una briciola di codice extra per emulare la loro funzionalità, allora il tuo overhead netto risultante è vincolato essere non meno, ma di più. Probabilmente, molto, molto, insondabilmente molto, di più.


7
"Virtual" è un termine C ++. Non ci sono metodi virtuali in Java. Esistono metodi ordinari, che sono polimorfici di runtime e metodi statici o finali, che non lo sono.
Zhenya

16
@levgen sì, per qualcuno il cui punto di vista è ristretto come la panoramica ufficiale di alto livello della lingua, è esattamente come dici tu. Ma ovviamente i concetti di alto livello sono implementati utilizzando meccanismi di basso livello ben consolidati che sono stati inventati molto tempo prima che Java venisse all'esistenza, e i metodi virtuali sono uno di questi. Se dai solo uno sguardo minuscolo sotto il cofano, vedrai immediatamente che è così: docs.oracle.com/javase/specs/jvms/se7/html/…
Mike Nakis

13
Grazie per aver risposto alla domanda senza fare presunzioni sull'ottimizzazione prematura. Bella risposta.
vegemite4me

3
Sì, è esattamente quello che volevo dire. Comunque ho appena eseguito il test sulla mia macchina. A parte il jitter che ci si può aspettare da un simile benchmark, non c'è alcuna differenza di velocità: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198sulla mia installazione di OpenJDK. FTR: Questo è vero anche se rimuovo il finalmodificatore. Btw. Dovevo terminatescendere in campo volatile, altrimenti la prova non finiva.
Marten

4
Cordiali saluti, ottengo risultati piuttosto sorprendenti su un Nexus 5 con sistema operativo Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Non solo OpenJDK sul mio notebook riesce a eseguire 40 volte più iterazioni, il test statico ha sempre circa il 30% in meno di throughput. Questo potrebbe essere un fenomeno specifico di ART, perché ottengo un risultato atteso su un tablet Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten

46

Bene, le chiamate statiche non possono essere sovrascritte (quindi sono sempre candidate per l'inlining) e non richiedono alcun controllo di nullità. HotSpot esegue una serie di fantastiche ottimizzazioni per i metodi di istanza che potrebbero negare questi vantaggi, ma sono possibili motivi per cui una chiamata statica potrebbe essere più veloce.

Tuttavia, ciò non dovrebbe influire sul tuo design - codice nel modo più leggibile e naturale - e preoccuparti di questo tipo di microottimizzazione solo se hai una giusta causa (cosa che quasi mai ).


Sono possibili motivi per cui una chiamata statica può essere più veloce Potresti spiegarmi questi motivi?
JavaTechnical

6
@JavaTechnical: La risposta spiega questi motivi: nessun override (il che significa che non è necessario elaborare l'implementazione da utilizzare ogni volta e puoi inline) e non devi controllare se stai chiamando il metodo su un riferimento nullo.
Jon Skeet

6
@JavaTechnical: non capisco. Ti ho appena dato cose che non hanno bisogno di essere calcolate / controllate per metodi statici, insieme a un'opportunità inlining. Non lavorare è un vantaggio in termini di prestazioni. Cosa resta da capire?
Jon Skeet

Le variabili statiche vengono recuperate più velocemente delle variabili non statiche?
JavaTechnical

1
@JavaTechnical: Beh, non c'è nessun controllo di nullità da eseguire, ma se il compilatore JIT può rimuovere quel controllo (che sarà specifico del contesto), non mi aspetterei molta differenza. Cose come se la memoria sia nella cache sarebbe molto più importante.
Jon Skeet

18

È specifico per compilatore / VM.

  • In teoria , una chiamata statica può essere resa leggermente più efficiente perché non ha bisogno di eseguire una ricerca di funzioni virtuali e può anche evitare l'overhead del parametro nascosto "this".
  • In pratica , molti compilatori lo ottimizzeranno comunque.

Quindi probabilmente non vale la pena preoccuparsi a meno che non si sia identificato come un problema di prestazioni veramente critico nella propria applicazione. L'ottimizzazione prematura è la radice di tutti i mali, ecc ...

Comunque ho già visto questa ottimizzazione dare un sostanziale aumento delle prestazioni nella seguente situazione:

  • Metodo che esegue un calcolo matematico molto semplice senza accessi alla memoria
  • Metodo invocato milioni di volte al secondo in un ciclo interno stretto
  • Applicazione vincolata alla CPU in cui ogni bit di prestazioni contava

Se quanto sopra si applica a te, potrebbe valere la pena testarlo.

C'è anche un'altra buona ragione (e potenzialmente ancora più importante!) Per usare un metodo statico - se il metodo ha effettivamente una semantica statica (cioè logicamente non è connesso a una data istanza della classe) allora ha senso renderlo statico per riflettere questo fatto. I programmatori Java esperti noteranno quindi il modificatore statico e penseranno immediatamente "aha! Questo metodo è statico quindi non necessita di un'istanza e presumibilmente non manipola lo stato specifico dell'istanza". Quindi avrai comunicato la natura statica del metodo in modo efficace ...


14

Come hanno detto i poster precedenti: questa sembra un'ottimizzazione prematura.

Tuttavia, c'è una differenza (a parte il fatto che le invocazioni non statiche richiedono un ulteriore push di un oggetto chiamato sullo stack di operandi):

Poiché i metodi statici non possono essere sovrascritti, non ci saranno ricerche virtuali in runtime per una chiamata al metodo statico. Ciò può comportare una differenza osservabile in alcune circostanze.

La differenza a livello di byte-codice è che una chiamata di metodo non statico viene fatto attraverso INVOKEVIRTUAL, INVOKEINTERFACEo INVOKESPECIALdurante una chiamata metodo statico viene fatto attraverso INVOKESTATIC.


2
Un metodo di istanza privata, tuttavia, viene (almeno tipicamente) invocato utilizzando invokespecialpoiché non è virtuale.
Mark Peters,

Ah, interessante, potevo solo pensare ai costruttori, ecco perché l'ho omesso! Grazie! (risposta in aggiornamento)
aioobe

2
La JVM ottimizzerà se viene istanziato un tipo. Se B estende A e nessuna istanza di B è stata istanziata, le chiamate al metodo su A non avranno bisogno di una ricerca nella tabella virtuale.
Steve Kuo,

13

È incredibilmente improbabile che qualsiasi differenza nelle prestazioni delle chiamate statiche rispetto a quelle non statiche stia facendo la differenza nella vostra applicazione. Ricorda che "l'ottimizzazione prematura è la radice di tutti i mali".



potresti spiegare ulteriormente cosa significa "" l'ottimizzazione prematura è la radice di tutti i mali "."?
user2121

La domanda era "C'è qualche vantaggio in termini di prestazioni in un modo o nell'altro?", E questo risponde esattamente a questa domanda.
DJClayworth

13

7 anni dopo ...

Non ho una grande fiducia nei risultati che Mike Nakis ha trovato perché non risolvono alcuni problemi comuni relativi alle ottimizzazioni dell'hotspot. Ho strumentato i benchmark utilizzando JMH e ho riscontrato che l'overhead di un metodo di istanza è di circa lo 0,75% sulla mia macchina rispetto a una chiamata statica. Considerato il basso overhead, penso che, tranne nelle operazioni più sensibili alla latenza, non sia probabilmente la più grande preoccupazione nella progettazione di un'applicazione. I risultati di riepilogo del mio benchmark JMH sono i seguenti;

java -jar target/benchmark.jar

# -- snip --

Benchmark                        Mode  Cnt          Score         Error  Units
MyBenchmark.testInstanceMethod  thrpt  200  414036562.933 ± 2198178.163  ops/s
MyBenchmark.testStaticMethod    thrpt  200  417194553.496 ± 1055872.594  ops/s

Puoi guardare il codice qui su Github;

https://github.com/nfisher/svsi

Il benchmark stesso è piuttosto semplice ma mira a ridurre al minimo l'eliminazione del codice morto e il ripiegamento costante. Probabilmente ci sono altre ottimizzazioni che ho perso / trascurato e questi risultati possono variare a seconda della versione e del sistema operativo di JVM.

package ca.junctionbox.svsi;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

class InstanceSum {
    public int sum(final int a, final int b) {
        return a + b;
    }
}

class StaticSum {
    public static int sum(final int a, final int b) {
        return a + b;
    }
}

public class MyBenchmark {
    private static final InstanceSum impl = new InstanceSum();

    @State(Scope.Thread)
    public static class Input {
        public int a = 1;
        public int b = 2;
    }

    @Benchmark
    public void testStaticMethod(Input i, Blackhole blackhole) {
        int sum = StaticSum.sum(i.a, i.b);
        blackhole.consume(sum);
    }

    @Benchmark
    public void testInstanceMethod(Input i, Blackhole blackhole) {
        int sum = impl.sum(i.a, i.b);
        blackhole.consume(sum);
    }
}

1
Interesse puramente accademico qui. Sono curioso di conoscere i potenziali vantaggi che questo tipo di microottimizzazione potrebbe avere su parametri diversi da quelli ops/sprincipalmente in un ambiente ART (ad esempio, utilizzo della memoria, dimensioni ridotte del file .oat, ecc.). Conosci strumenti / modi relativamente semplici in cui si potrebbe tentare di confrontare queste altre metriche?
Ryan Thomas,

Hotspot calcola che non ci sono estensioni per InstanceSum nel classpath. Prova ad aggiungere un'altra classe che estende InstanceSum e sostituisce il metodo.
milano

12

Per decidere se un metodo debba essere statico, l'aspetto delle prestazioni dovrebbe essere irrilevante. Se hai un problema di prestazioni, rendere statici molti metodi non salverà la giornata. Detto questo, i metodi statici quasi certamente non sono più lenti di qualsiasi metodo di istanza, nella maggior parte dei casi leggermente più veloce :

1.) I metodi statici non sono polimorfici, quindi la JVM ha meno decisioni da prendere per trovare il codice effettivo da eseguire. Questo è un punto controverso in Age of Hotspot, poiché Hotspot ottimizzerà le chiamate al metodo di istanza che hanno un solo sito di implementazione, quindi eseguiranno lo stesso.

2.) Un'altra sottile differenza è che i metodi statici ovviamente non hanno alcun riferimento a "questo". Ciò si traduce in uno stack frame uno slot più piccolo di quello di un metodo di istanza con la stessa firma e corpo ("this" viene messo nello slot 0 delle variabili locali a livello di bytecode, mentre per i metodi statici lo slot 0 viene utilizzato per il primo parametro del metodo).


5

Potrebbe esserci una differenza e potrebbe andare in entrambi i modi per qualsiasi particolare pezzo di codice e potrebbe cambiare anche con una versione minore della JVM.

Questo fa sicuramente parte del 97% delle piccole efficienze che dovresti dimenticare .


2
Sbagliato. Non puoi dare per scontato nulla. Potrebbe essere un ciclo stretto richiesto per un'interfaccia utente front-end che potrebbe fare un'enorme differenza su quanto sia "scattante" l'interfaccia utente. Ad esempio, una ricerca TableViewdi milioni di record.
trilogia

0

In teoria, meno costoso.

L'inizializzazione statica verrà eseguita anche se si crea un'istanza dell'oggetto, mentre i metodi statici non eseguiranno alcuna inizializzazione normalmente eseguita in un costruttore.

Tuttavia, non l'ho testato.


1
@R. Bemrose, cosa c'entra l'inizializzazione statica con questa domanda?
Kirk Woll,

@ Kirk Woll: poiché l'inizializzazione statica viene eseguita la prima volta che si fa riferimento alla classe ... anche prima della prima chiamata al metodo statico.
Powerlord

@R. Bemrose, certo, come sta caricando la classe nella VM per cominciare. Sembra un non sequestratore, IMO.
Kirk Woll,

0

Come osserva Jon, i metodi statici non possono essere sovrascritti, quindi semplicemente invocare un metodo statico potrebbe essere - su un runtime Java sufficientemente ingenuo - più veloce che invocare un metodo di istanza.

Ma poi, anche supponendo che tu sia al punto in cui ti interessa rovinare il tuo progetto per risparmiare qualche nanosecondo, questo solleva solo un'altra domanda: avrai bisogno di un metodo che preveda te stesso? Se cambi il tuo codice per trasformare un metodo di istanza in un metodo statico per salvare un nanosecondo qua e là, e poi ti giri e implementi il ​​tuo dispatcher in cima a quello, il tuo sarà quasi certamente meno efficiente di quello costruito già nel tuo runtime Java.


-2

Vorrei aggiungere alle altre ottime risposte qui che dipende anche dal tuo flusso, ad esempio:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

Fai attenzione a creare un nuovo oggetto MyRowMapper per ogni chiamata.
Suggerisco invece di utilizzare qui un campo statico.

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
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.