Gibt es auf die eine oder andere Weise Leistungsvorteile? Ist es Compiler / VM-spezifisch? Ich benutze Hotspot.
Gibt es auf die eine oder andere Weise Leistungsvorteile? Ist es Compiler / VM-spezifisch? Ich benutze Hotspot.
Antworten:
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).
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 this
Zeiger 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.
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
bei meiner OpenJDK-Installation. FTR: Das stimmt sogar, wenn ich den final
Modifikator entferne . Übrigens. Ich musste das terminate
Feld machen volatile
, sonst wurde der Test nicht beendet.
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
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).
Es ist compiler- / VM-spezifisch.
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:
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 ...
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
, INVOKEINTERFACE
oder INVOKESPECIAL
während eines statischen Methodenaufruf durch erfolgt INVOKESTATIC
.
invokespecial
da sie nicht virtuell ist.
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".
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);
}
}
ops/s
Mikrooptimierung 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?
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).
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 .
TableView
nach Millionen von Datensätzen.
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.
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.
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);
};
};