Sind statische Java-Aufrufe mehr oder weniger teuer als nicht statische Aufrufe?

95

Gibt es auf die eine oder andere Weise Leistungsvorteile? Ist es Compiler / VM-spezifisch? Ich benutze Hotspot.

Andy Faibishenko
quelle

Antworten:

74

Erstens: Sie sollten nicht die Wahl zwischen statisch und nicht statisch auf der Grundlage der Leistung treffen.

Zweitens: In der Praxis macht es keinen Unterschied. Hotspot kann sich für eine Optimierung entscheiden, die statische Aufrufe für eine Methode schneller und nicht statische Aufrufe für eine andere Methode beschleunigt.

Drittens: Ein Großteil des Mythos um statische und nicht statische Elemente basiert entweder auf sehr alten JVMs (die nicht annähernd der von Hotspot vorgenommenen Optimierung entsprachen) oder auf einigen bekannten Trivia zu C ++ (bei denen ein dynamischer Aufruf einen weiteren Speicherzugriff verwendet) als ein statischer Anruf).

Anon
quelle
1
Sie haben absolut Recht, Sie sollten statische Methoden nicht bevorzugen, die allein darauf basieren. Wenn statische Methoden jedoch gut zum Design passen, ist es hilfreich zu wissen, dass sie mindestens genauso schnell sind, wenn nicht sogar schneller als Instanzmethoden und nicht auf Leistungsbasis ausgeschlossen werden sollten.
Will
2
@AaronDigulla -.- Was wäre, wenn ich dir sagen würde, dass ich hierher gekommen bin, weil ich gerade optimiere, nicht vorzeitig, aber wenn ich es wirklich brauche? Sie haben angenommen, dass OP vorzeitig optimieren möchte, aber Sie wissen, dass diese Site irgendwie global ist ... Richtig? Ich möchte nicht unhöflich sein, aber bitte nehmen Sie solche Dinge beim nächsten Mal nicht an.
Dalibor Filus
1
@ DaliborFilus Ich muss ein Gleichgewicht finden. Die Verwendung statischer Methoden verursacht alle Arten von Problemen. Sie sollten daher vermieden werden, insbesondere wenn Sie nicht wissen, was Sie tun. Zweitens ist der meiste "langsame" Code auf (schlechtes) Design zurückzuführen, nicht darauf, dass die Sprache der Wahl langsam ist. Wenn Ihr Code langsam ist, werden statische Methoden ihn wahrscheinlich nur speichern, wenn seine aufrufenden Methoden absolut nichts bewirken . In den meisten Fällen wird der Code in den Methoden den Aufrufaufwand in den Schatten stellen.
Aaron Digulla
6
Abgestimmt. Dies beantwortet die Frage nicht. Die Frage nach den Leistungsvorteilen. Es wurden keine Meinungen zu Gestaltungsprinzipien eingeholt.
Colm Bhandal
4
Wenn ich einen Papagei trainieren würde, um zu sagen, "vorzeitige Opimisierung ist die Wurzel allen Übels", würde ich 1000 Stimmen von Leuten bekommen, die so viel über Leistung wissen wie der Papagei.
rghome
62

Vier Jahre später...

Okay, in der Hoffnung, diese Frage ein für alle Mal zu klären, habe ich einen Benchmark geschrieben, der zeigt, wie die verschiedenen Arten von Anrufen (virtuell, nicht virtuell, statisch) miteinander verglichen werden.

Ich habe es auf ideone ausgeführt , und das habe ich bekommen:

(Eine größere Anzahl von Iterationen ist besser.)

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

Wie erwartet sind virtuelle Methodenaufrufe die langsamsten, nicht virtuelle Methodenaufrufe sind schneller und statische Methodenaufrufe sind noch schneller.

Was ich nicht erwartet hatte, war, dass die Unterschiede so ausgeprägt waren: Es wurde gemessen, dass virtuelle Methodenaufrufe mit weniger als der Hälfte der Geschwindigkeit nicht virtueller Methodenaufrufe ausgeführt wurden, die wiederum ganze 15% langsamer als statische Aufrufe ausgeführt wurden. Das zeigen diese Messungen; Die tatsächlichen Unterschiede müssen in der Tat etwas ausgeprägter sein, da mein Benchmarking-Code für jeden virtuellen, nicht virtuellen und statischen Methodenaufruf einen zusätzlichen konstanten Aufwand hat, eine ganzzahlige Variable zu inkrementieren, eine boolesche Variable zu überprüfen und eine Schleife durchzuführen, wenn dies nicht der Fall ist.

Ich nehme an, die Ergebnisse variieren von CPU zu CPU und von JVM zu JVM. Probieren Sie es also aus und sehen Sie, was Sie erhalten:

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

Es ist anzumerken, dass dieser Leistungsunterschied nur für Code gilt, der nichts anderes tut, als parameterlose Methoden aufzurufen. Unabhängig davon, welchen anderen Code Sie zwischen den Aufrufen haben, werden die Unterschiede verringert, und dies schließt die Parameterübergabe ein. Tatsächlich wird der Unterschied von 15% zwischen statischen und nicht virtuellen Aufrufen wahrscheinlich vollständig durch die Tatsache erklärt , dass der thisZeiger nicht an die statische Methode übergeben werden muss. Es würde also nur eine relativ kleine Menge Code erfordern, um zwischen den Aufrufen triviale Dinge zu erledigen, damit der Unterschied zwischen verschiedenen Arten von Aufrufen so weit verwässert wird, dass keinerlei Nettoauswirkungen mehr auftreten.

Auch virtuelle Methodenaufrufe existieren aus einem bestimmten Grund. Sie haben einen Zweck zu erfüllen und werden mit den effizientesten Mitteln implementiert, die von der zugrunde liegenden Hardware bereitgestellt werden. (Der CPU-Befehlssatz.) Wenn Sie in Ihrem Wunsch, sie durch Ersetzen durch nicht virtuelle oder statische Aufrufe zu beseitigen, bis zu einem Jota zusätzlichen Codes hinzufügen müssen, um ihre Funktionalität zu emulieren, ist Ihr resultierender Netto-Overhead gebunden nicht weniger sein, sondern mehr. Möglicherweise viel, viel, unergründlich viel, mehr.

Mike Nakis
quelle
7
'Virtual' ist ein C ++ - Begriff. In Java gibt es keine virtuellen Methoden. Es gibt gewöhnliche Methoden, die zur Laufzeit polymorph sind, und statische oder endgültige Methoden, die es nicht sind.
Zhenya
16
@levgen ja, für jemanden, dessen Standpunkt so eng ist wie der offizielle Überblick über die Sprache auf hoher Ebene, ist es genau so, wie Sie sagen. Aber natürlich werden die Konzepte auf hoher Ebene mithilfe gut etablierter Mechanismen auf niedriger Ebene implementiert, die lange vor der Entstehung von Java erfunden wurden, und virtuelle Methoden sind eine davon. Wenn Sie nur einen winzigen Blick unter die Haube werfen, werden Sie sofort feststellen, dass dies so ist: docs.oracle.com/javase/specs/jvms/se7/html/…
Mike Nakis
13
Vielen Dank, dass Sie die Frage beantwortet haben, ohne Vermutungen über eine vorzeitige Optimierung anzustellen. Gute Antwort.
vegemite4me
3
Ja, genau das habe ich gemeint. Jedenfalls habe ich den Test gerade auf meiner Maschine durchgeführt. Abgesehen von dem Jitter, den Sie für einen solchen Benchmark erwarten können, gibt es keinerlei Geschwindigkeitsunterschied: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198bei meiner OpenJDK-Installation. FTR: Das stimmt sogar, wenn ich den finalModifikator entferne . Übrigens. Ich musste das terminateFeld machen volatile, sonst wurde der Test nicht beendet.
Marten
4
Zu Ihrer Information, ich bekomme ziemlich überraschende Ergebnisse auf einem Nexus 5 mit Android 6 : VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Nicht nur, dass OpenJDK auf meinem Notebook 40-mal mehr Iterationen ausführt, der statische Test hat immer einen um 30% geringeren Durchsatz. Dies könnte ein ART-spezifisches Phänomen sein, da ich auf einem Android 4.4-Tablet ein erwartetes Ergebnis VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
46

Nun, statische Aufrufe können nicht überschrieben werden (sind also immer Kandidaten für Inlining) und erfordern keine Nichtigkeitsprüfungen. HotSpot führt eine Reihe cooler Optimierungen durch, zum Beispiel Methoden, die diese Vorteile möglicherweise zunichte machen, aber sie sind mögliche Gründe, warum ein statischer Aufruf schneller sein kann.

Dies sollte sich jedoch nicht auf Ihr Design auswirken - Code auf die lesbarste und natürlichste Weise - und sich nur dann um diese Art der Mikrooptimierung kümmern, wenn Sie nur einen Grund haben (den Sie so gut wie nie werden).

Jon Skeet
quelle
Dies sind mögliche Gründe, warum ein statischer Anruf schneller sein kann. Können Sie mir diese Gründe erklären?
JavaTechnical
6
@JavaTechnical: Die Antwort erklärt diese Gründe - kein Überschreiben (was bedeutet, dass Sie die Implementierung nicht jedes Mal ausarbeiten müssen und inline verwenden können) und Sie müssen nicht überprüfen, ob Sie die Methode auf a aufrufen Nullreferenz.
Jon Skeet
6
@ JavaTechnical: Ich verstehe nicht. Ich habe Ihnen gerade Dinge gegeben, die nicht für statische Methoden berechnet / überprüft werden müssen, zusammen mit einer Inlining-Möglichkeit. Nicht arbeiten ist ein Leistungsvorteil. Was bleibt zu verstehen?
Jon Skeet
Werden statische Variablen schneller abgerufen als nicht statische Variablen?
JavaTechnical
1
@JavaTechnical: Nun, es muss keine Nichtigkeitsprüfung durchgeführt werden - aber wenn der JIT-Compiler diese Prüfung (die kontextspezifisch sein wird) entfernen kann, würde ich keinen großen Unterschied erwarten. Dinge wie, ob sich der Speicher im Cache befindet, wären viel wichtiger.
Jon Skeet
18

Es ist compiler- / VM-spezifisch.

  • Theoretisch kann ein statischer Aufruf etwas effizienter gestaltet werden, da keine virtuelle Funktionssuche durchgeführt werden muss und der Overhead des versteckten "this" -Parameters vermieden werden kann.
  • In der Praxis werden viele Compiler dies ohnehin optimieren.

Daher lohnt es sich wahrscheinlich nicht, sich darum zu kümmern, es sei denn, Sie haben dies als ein wirklich kritisches Leistungsproblem in Ihrer Anwendung identifiziert. Vorzeitige Optimierung ist die Wurzel allen Übels usw.

Ich habe jedoch gesehen, dass diese Optimierung in der folgenden Situation zu einer erheblichen Leistungssteigerung führt:

  • Methode zur Durchführung einer sehr einfachen mathematischen Berechnung ohne Speicherzugriffe
  • Die Methode wird millionenfach pro Sekunde in einer engen inneren Schleife aufgerufen
  • CPU-gebundene Anwendung, bei der jede Leistung eine Rolle spielt

Wenn das oben Gesagte auf Sie zutrifft, kann es sich lohnen, es zu testen.

Es gibt noch einen weiteren guten (und möglicherweise sogar noch wichtigeren!) Grund, eine statische Methode zu verwenden: Wenn die Methode tatsächlich eine statische Semantik aufweist (dh logisch nicht mit einer bestimmten Instanz der Klasse verbunden ist), ist es sinnvoll, sie statisch zu machen diese Tatsache zu reflektieren. Erfahrene Java-Programmierer werden dann den statischen Modifikator bemerken und sofort denken "aha! Diese Methode ist statisch, benötigt also keine Instanz und manipuliert vermutlich nicht den instanzspezifischen Status". Sie haben also die statische Natur der Methode effektiv kommuniziert ...

mikera
quelle
14

Wie in früheren Postern bereits erwähnt: Dies scheint eine vorzeitige Optimierung zu sein.

Es gibt jedoch einen Unterschied (ein Teil der Tatsache, dass nicht statische Aufrufe einen zusätzlichen Druck eines Angerufenen auf den Operandenstapel erfordern):

Da statische Methoden nicht überschrieben werden können, werden zur Laufzeit keine virtuellen Suchvorgänge für einen statischen Methodenaufruf durchgeführt. Dies kann unter Umständen zu einem beobachtbaren Unterschied führen.

Die Differenz auf einer Byte-Code - Ebene ist , daß ein nicht statischer Methodenaufruf wird durch getan INVOKEVIRTUAL, INVOKEINTERFACEoder INVOKESPECIALwährend eines statischen Methodenaufruf durch erfolgt INVOKESTATIC.

aioobe
quelle
2
Eine private Instanzmethode wird jedoch (zumindest normalerweise) mit aufgerufen, invokespecialda sie nicht virtuell ist.
Mark Peters
Ah, interessant, ich konnte nur an Konstrukteure denken, deshalb habe ich es weggelassen! Vielen Dank! (Antwort aktualisieren)
aioobe
2
Die JVM wird optimiert, wenn ein Typ instanziiert wird. Wenn B A erweitert und keine Instanz von B instanziiert wurde, benötigen Methodenaufrufe für A keine Suche nach virtuellen Tabellen.
Steve Kuo
13

Es ist unglaublich unwahrscheinlich, dass ein Unterschied in der Leistung von statischen und nicht statischen Aufrufen einen Unterschied in Ihrer Anwendung bewirkt. Denken Sie daran, dass "vorzeitige Optimierung die Wurzel allen Übels ist".

DJClayworth
quelle
Es war Knuth! shreevatsa.wordpress.com/2008/05/16/…
ShreevatsaR
Würden Sie bitte weiter erklären, was "" vorzeitige Optimierung ist die Wurzel allen Übels "?"
user2121
Die Frage lautete "Gibt es auf die eine oder andere Weise einen Leistungsvorteil?", Und genau diese Frage wird beantwortet.
DJClayworth
13

7 Jahre später ...

Ich habe kein großes Vertrauen in die Ergebnisse, die Mike Nakis gefunden hat, weil sie einige häufig auftretende Probleme im Zusammenhang mit Hotspot-Optimierungen nicht ansprechen. Ich habe Benchmarks mit JMH instrumentiert und festgestellt, dass der Overhead einer Instanzmethode auf meinem Computer etwa 0,75% gegenüber einem statischen Aufruf beträgt. Angesichts des geringen Overheads denke ich, dass dies außer bei den latenzempfindlichsten Vorgängen wohl nicht das größte Problem bei einem Anwendungsdesign ist. Die zusammenfassenden Ergebnisse meines JMH-Benchmarks lauten wie folgt:

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

Sie können den Code hier auf Github ansehen.

https://github.com/nfisher/svsi

Der Benchmark selbst ist ziemlich einfach, zielt jedoch darauf ab, die Eliminierung von totem Code und das ständige Falten zu minimieren. Es gibt möglicherweise andere Optimierungen, die ich übersehen habe, und diese Ergebnisse variieren wahrscheinlich je nach JVM-Version und Betriebssystem.

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);
    }
}
Nathan
quelle
1
Rein akademisches Interesse hier. Ich bin gespannt auf mögliche Vorteile, die diese Art der ops/sMikrooptimierung für andere Metriken als hauptsächlich in einer ART-Umgebung haben könnte (z. B. Speichernutzung, reduzierte .oat-Dateigröße usw.). Kennen Sie relativ einfache Tools / Methoden, mit denen Sie versuchen könnten, diese anderen Metriken zu vergleichen?
Ryan Thomas
Hotspot stellt fest, dass es im Klassenpfad keine Erweiterungen für InstanceSum gibt. Versuchen Sie, eine weitere Klasse hinzuzufügen, die InstanceSum erweitert und die Methode überschreibt.
Mailand,
12

Für die Entscheidung, ob eine Methode statisch sein soll, sollte der Leistungsaspekt irrelevant sein. Wenn Sie ein Leistungsproblem haben, können Sie den Tag nicht retten, wenn Sie viele Methoden statisch machen. Allerdings sind statische Methoden mit ziemlicher Sicherheit nicht langsamer als jede Instanzmethode, in den meisten Fällen geringfügig schneller :

1.) Statische Methoden sind nicht polymorph, daher muss die JVM weniger Entscheidungen treffen, um den tatsächlich auszuführenden Code zu finden. Dies ist ein strittiger Punkt im Zeitalter von Hotspot, da Hotspot Instanzmethodenaufrufe mit nur einer Implementierungssite optimiert, sodass sie dieselbe Leistung erbringen.

2.) Ein weiterer subtiler Unterschied besteht darin, dass statische Methoden offensichtlich keine "diese" Referenz haben. Dies führt zu einem Stapelrahmen, der einen Steckplatz kleiner als der einer Instanzmethode mit derselben Signatur und demselben Text ist ("dies" wird in Steckplatz 0 der lokalen Variablen auf Bytecode-Ebene eingefügt, während für statische Methoden Steckplatz 0 für den ersten verwendet wird Parameter der Methode).

Durandal
quelle
5

Es kann einen Unterschied geben, und es kann für einen bestimmten Code in beide Richtungen gehen, und es kann sich sogar mit einer geringfügigen Veröffentlichung der JVM ändern.

Dies ist definitiv Teil der 97% der kleinen Wirkungsgrade, die Sie vergessen sollten .

Michael Borgwardt
quelle
2
Falsch. Sie können nichts annehmen. Es könnte eine enge Schleife sein, die für eine Front-End-Benutzeroberfläche erforderlich ist, was einen großen Unterschied darin machen könnte, wie "bissig" die Benutzeroberfläche ist. Zum Beispiel eine Suche TableViewnach Millionen von Datensätzen.
Trilogie
0

Theoretisch günstiger.

Die statische Initialisierung wird auch dann durchgeführt, wenn Sie eine Instanz des Objekts erstellen, während statische Methoden keine Initialisierung durchführen, die normalerweise in einem Konstruktor durchgeführt wird.

Ich habe dies jedoch nicht getestet.

Powerlord
quelle
1
@R. Bemrose, was hat statische Initialisierung mit dieser Frage zu tun?
Kirk Woll
@Kirk Woll: Da die statische Initialisierung beim ersten Referenzieren der Klasse erfolgt ... auch vor dem ersten statischen Methodenaufruf.
Powerlord
@R. Bemrose ist sicher, dass die Klasse zunächst in die VM geladen wird. Scheint wie ein Nicht-Sequitor, IMO.
Kirk Woll
0

Wie Jon bemerkt, können statische Methoden nicht überschrieben werden. Das einfache Aufrufen einer statischen Methode kann daher bei einer ausreichend naiven Java-Laufzeit schneller sein als das Aufrufen einer Instanzmethode.

Aber selbst wenn Sie an dem Punkt angelangt sind, an dem Sie Ihr Design durcheinander bringen möchten, um ein paar Nanosekunden zu sparen, wirft dies nur eine andere Frage auf: Brauchen Sie eine Methode, die sich selbst überschreibt? Wenn Sie Ihren Code ändern, um eine Instanzmethode in eine statische Methode umzuwandeln, um hier und da eine Nanosekunde zu sparen, und sich dann umdrehen und Ihren eigenen Dispatcher implementieren, ist Ihr Dispatcher mit ziemlicher Sicherheit weniger effizient als der erstellte bereits in Ihre Java-Laufzeit.

Ken
quelle
-2

Ich möchte den anderen großartigen Antworten hier hinzufügen, dass es auch von Ihrem Fluss abhängt, zum Beispiel:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

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

Achten Sie darauf, dass Sie bei jedem Aufruf ein neues MyRowMapper-Objekt erstellen.
Stattdessen schlage ich vor, hier ein statisches Feld zu verwenden.

Public class MyDao {

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

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
Yair Zaslavsky
quelle