Java Performance Programming, Teil 2: Die Kosten für das Casting

Bei diesem zweiten Artikel in unserer Reihe zur Java-Leistung liegt der Schwerpunkt auf dem Casting - was es ist, was es kostet und wie wir es (manchmal) vermeiden können. In diesem Monat beginnen wir mit einem kurzen Überblick über die Grundlagen von Klassen, Objekten und Referenzen und werfen anschließend einen Blick auf einige Hardcore-Leistungsdaten (in einer Seitenleiste, um die Zimperlichkeit nicht zu beleidigen!) Und Richtlinien für die Arten von Vorgängen, die am wahrscheinlichsten zu einer Verdauungsstörung Ihrer Java Virtual Machine (JVM) führen. Abschließend untersuchen wir ausführlich, wie wir häufige Klassenstrukturierungseffekte vermeiden können, die zu Casting führen können.

Java Performance Programming: Lesen Sie die ganze Serie!

  • Teil 1. Erfahren Sie, wie Sie den Programmaufwand reduzieren und die Leistung verbessern, indem Sie die Objekterstellung und die Speicherbereinigung steuern
  • Teil 2. Reduzieren Sie Overhead- und Ausführungsfehler durch typsicheren Code
  • Teil 3. Sehen Sie, wie Sammlungsalternativen die Leistung messen, und finden Sie heraus, wie Sie die einzelnen Typen optimal nutzen können

Objekt- und Referenztypen in Java

Im letzten Monat haben wir die grundlegende Unterscheidung zwischen primitiven Typen und Objekten in Java diskutiert. Sowohl die Anzahl der primitiven Typen als auch die Beziehungen zwischen ihnen (insbesondere Konvertierungen zwischen Typen) werden durch die Sprachdefinition festgelegt. Objekte hingegen sind von unbegrenzter Art und können mit einer beliebigen Anzahl anderer Typen in Beziehung gesetzt werden.

Jede Klassendefinition in einem Java-Programm definiert einen neuen Objekttyp. Dies schließt alle Klassen aus den Java-Bibliotheken ein, sodass jedes Programm Hunderte oder sogar Tausende verschiedener Objekttypen verwenden kann. Einige dieser Typen werden in der Java-Sprachdefinition als mit bestimmten speziellen Verwendungen oder Handhabungen (z. B. der Verwendung java.lang.StringBufferfür java.lang.StringVerkettungsvorgänge) spezifiziert . Abgesehen von diesen wenigen Ausnahmen werden jedoch alle Typen vom Java-Compiler und der zur Ausführung des Programms verwendeten JVM grundsätzlich gleich behandelt.

Wenn eine Klassendefinition (mithilfe der extendsKlausel im Klassendefinitionsheader) keine andere Klasse als übergeordnete oder übergeordnete Klasse angibt, wird die java.lang.ObjectKlasse implizit erweitert . Dies bedeutet, dass sich jede Klasse letztendlich java.lang.Objectentweder direkt oder über eine Folge von einer oder mehreren Ebenen übergeordneter Klassen erstreckt.

Objekte selbst sind immer Instanzen von Klassen, und der Typ eines Objekts ist die Klasse, deren Instanz es ist. In Java beschäftigen wir uns jedoch nie direkt mit Objekten. Wir arbeiten mit Verweisen auf Objekte. Zum Beispiel die Zeile:

 java.awt.Component myComponent; 

erstellt kein java.awt.ComponentObjekt; Es wird eine Referenzvariable vom Typ erstellt java.lang.Component. Obwohl Referenzen genau wie Objekte Typen haben, gibt es keine genaue Übereinstimmung zwischen Referenz- und Objekttypen - ein Referenzwert kann nullein Objekt des gleichen Typs wie die Referenz oder ein Objekt einer Unterklasse (dh einer Klasse, die absteigt) sein von) die Art der Referenz. In diesem speziellen Fall java.awt.Componenthandelt es sich um eine abstrakte Klasse, sodass wir wissen, dass es niemals ein Objekt des gleichen Typs wie unsere Referenz geben kann, aber es kann sicherlich Objekte von Unterklassen dieses Referenztyps geben.

Polymorphismus und Gießen

Der Typ einer Referenz bestimmt, wie das referenzierte Objekt , dh das Objekt, das den Wert der Referenz darstellt, verwendet werden kann. Im obigen Beispiel myComponentkönnte Code using beispielsweise eine der von der Klasse definierten Methoden java.awt.Componentoder eine ihrer Oberklassen für das referenzierte Objekt aufrufen .

Die tatsächlich von einem Aufruf ausgeführte Methode wird jedoch nicht durch den Typ der Referenz selbst bestimmt, sondern durch den Typ des referenzierten Objekts. Dies ist das Grundprinzip des Polymorphismus - Unterklassen können in der übergeordneten Klasse definierte Methoden überschreiben, um ein anderes Verhalten zu implementieren. Wenn im Fall unserer Beispielvariablen das referenzierte Objekt tatsächlich eine Instanz von wäre java.awt.Button, würde sich die Statusänderung, die sich aus einem setLabel("Push Me")Aufruf ergibt, von der ändern , die sich ergibt, wenn das referenzierte Objekt eine Instanz von wäre java.awt.Label.

Neben Klassendefinitionen verwenden Java-Programme auch Schnittstellendefinitionen. Der Unterschied zwischen einer Schnittstelle und einer Klasse besteht darin, dass eine Schnittstelle nur eine Reihe von Verhaltensweisen (und in einigen Fällen Konstanten) angibt, während eine Klasse eine Implementierung definiert. Da Schnittstellen keine Implementierungen definieren, können Objekte niemals Instanzen einer Schnittstelle sein. Dies können jedoch Instanzen von Klassen sein, die eine Schnittstelle implementieren. Referenzen können vom Schnittstellentyp sein. In diesem Fall können die referenzierten Objekte Instanzen einer Klasse sein, die die Schnittstelle implementiert (entweder direkt oder über eine Vorgängerklasse).

Casting wird verwendet, um zwischen Typen zu konvertieren - insbesondere zwischen Referenztypen, für die Art des Casting-Vorgangs, an dem wir hier interessiert sind. Upcast-Vorgänge ( in der Java-Sprachspezifikation auch als Erweiterungskonvertierungen bezeichnet ) konvertieren eine Unterklassenreferenz in eine Vorgängerklassenreferenz . Dieser Casting-Vorgang erfolgt normalerweise automatisch, da er immer sicher ist und direkt vom Compiler implementiert werden kann.

Downcast-Operationen ( in der Java-Sprachspezifikation auch als Verengungskonvertierungen bezeichnet ) konvertieren eine Vorgängerklassenreferenz in eine Unterklassenreferenz. Dieser Casting-Vorgang verursacht einen Ausführungsaufwand, da Java erfordert, dass der Cast zur Laufzeit überprüft wird, um sicherzustellen, dass er gültig ist. Wenn das referenzierte Objekt weder eine Instanz des Zieltyps für die Umwandlung noch eine Unterklasse dieses Typs ist, ist die versuchte Umwandlung nicht zulässig und muss a auslösen java.lang.ClassCastException.

Mit dem instanceofOperator in Java können Sie bestimmen, ob eine bestimmte Casting-Operation zulässig ist, ohne die Operation tatsächlich zu versuchen. Da die Leistungskosten einer Prüfung viel geringer sind als die der Ausnahme, die durch einen unzulässigen Besetzungsversuch generiert wird, ist es im Allgemeinen ratsam, einen instanceofTest immer dann zu verwenden , wenn Sie nicht sicher sind, ob der Typ einer Referenz dem entspricht, den Sie möchten . Bevor Sie dies tun, sollten Sie jedoch sicherstellen, dass Sie eine vernünftige Möglichkeit haben, mit einer Referenz eines unerwünschten Typs umzugehen. Andernfalls können Sie die Ausnahme auch einfach auslösen lassen und sie auf einer höheren Ebene in Ihrem Code behandeln.

Vorsicht walten lassen

Das Casting ermöglicht die Verwendung der generischen Programmierung in Java, wo Code geschrieben wird, um mit allen Objekten von Klassen zu arbeiten, die von einer Basisklasse abstammen (häufig java.lang.Objectfür Dienstprogrammklassen). Die Verwendung von Guss verursacht jedoch eine einzigartige Reihe von Problemen. Im nächsten Abschnitt werden wir uns mit den Auswirkungen auf die Leistung befassen. Betrachten wir jedoch zunächst die Auswirkungen auf den Code selbst. Hier ist ein Beispiel mit der generischen java.lang.VectorAuflistungsklasse:

privater Vektor someNumbers; ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...}

Dieser Code weist potenzielle Probleme hinsichtlich Klarheit und Wartbarkeit auf. Wenn jemand anders als der ursprüngliche Entwickler den Code irgendwann ändern würde, könnte er vernünftigerweise denken, dass er java.lang.Doubleden someNumbersSammlungen ein hinzufügen könnte , da dies eine Unterklasse von ist java.lang.Number. Alles würde gut kompilieren, wenn er dies versuchte, aber an einem unbestimmten Punkt in der Ausführung würde er wahrscheinlich einen Wurf bekommen java.lang.ClassCastException, wenn die versuchte Besetzung zu a java.lang.Integerfür seinen Mehrwert ausgeführt wurde.

Das Problem hierbei ist, dass die Verwendung von Casting die im Java-Compiler integrierten Sicherheitsüberprüfungen umgeht. Der Programmierer sucht während der Ausführung nach Fehlern, da der Compiler sie nicht abfängt. Dies ist an und für sich nicht katastrophal, aber diese Art von Verwendungsfehler verbirgt sich oft recht geschickt, während Sie Ihren Code testen, nur um sich zu zeigen, wenn das Programm in Produktion geht.

Es überrascht nicht, dass die Unterstützung einer Technik, mit der der Compiler diese Art von Verwendungsfehlern erkennen kann, eine der am häufigsten nachgefragten Verbesserungen von Java ist. Derzeit läuft ein Projekt im Java Community-Prozess, in dem untersucht wird, wie genau diese Unterstützung hinzugefügt werden kann: Projektnummer JSR-000014, Hinzufügen generischer Typen zur Java-Programmiersprache (weitere Informationen finden Sie im Abschnitt Ressourcen unten.) In der Fortsetzung dieses Artikels Im nächsten Monat werden wir uns dieses Projekt genauer ansehen und diskutieren, wie es wahrscheinlich helfen wird und wo wir wahrscheinlich mehr wollen.

Das Leistungsproblem

Es ist seit langem bekannt, dass das Casting die Leistung in Java beeinträchtigen kann und dass Sie die Leistung verbessern können, indem Sie das Casting in häufig verwendetem Code minimieren. Methodenaufrufe, insbesondere Aufrufe über Schnittstellen, werden häufig auch als potenzielle Leistungsengpässe genannt. Die aktuelle Generation von JVMs hat jedoch einen langen Weg von ihren Vorgängern zurückgelegt, und es lohnt sich zu prüfen, wie gut diese Prinzipien heute Bestand haben.

Für diesen Artikel habe ich eine Reihe von Tests entwickelt, um festzustellen, wie wichtig diese Faktoren für die Leistung mit aktuellen JVMs sind. Die Testergebnisse sind in zwei Tabellen in der Seitenleiste zusammengefasst: Tabelle 1 zeigt den Overhead für Methodenaufrufe und Tabelle 2 den Overhead für Casting. Der vollständige Quellcode für das Testprogramm ist auch online verfügbar (weitere Informationen finden Sie im Abschnitt Ressourcen unten).

Um diese Schlussfolgerungen für Leser zusammenzufassen, die die Details in den Tabellen nicht durchgehen möchten, sind bestimmte Arten von Methodenaufrufen und Casts immer noch recht teuer und dauern in einigen Fällen fast so lange wie eine einfache Objektzuweisung. Wenn möglich, sollten diese Arten von Vorgängen in Code vermieden werden, der für die Leistung optimiert werden muss.

Insbesondere Aufrufe überschriebener Methoden (Methoden, die in einer geladenen Klasse überschrieben werden, nicht nur in der tatsächlichen Klasse des Objekts) und Aufrufe über Schnittstellen sind erheblich teurer als einfache Methodenaufrufe. Die im Test verwendete Beta-Version von HotSpot Server JVM 2.0 konvertiert sogar viele einfache Methodenaufrufe in Inline-Code, wodurch jeglicher Aufwand für solche Vorgänge vermieden wird. HotSpot zeigt jedoch die schlechteste Leistung unter den getesteten JVMs für überschriebene Methoden und Aufrufe über Schnittstellen.

Für das Casting (natürlich Downcasting) halten die getesteten JVMs die Leistung im Allgemeinen auf einem vernünftigen Niveau. HotSpot leistet damit bei den meisten Benchmark-Tests hervorragende Arbeit und kann, wie bei den Methodenaufrufen, in vielen einfachen Fällen den Overhead des Castings fast vollständig eliminieren. In komplizierteren Situationen, wie z. B. Casts, gefolgt von Aufrufen überschriebener Methoden, weisen alle getesteten JVMs einen spürbaren Leistungsabfall auf.

Die getestete Version von HotSpot zeigte auch eine extrem schlechte Leistung, wenn ein Objekt nacheinander in verschiedene Referenztypen umgewandelt wurde (anstatt immer in denselben Zieltyp umgewandelt zu werden). Diese Situation tritt regelmäßig in Bibliotheken wie Swing auf, die eine tiefe Hierarchie von Klassen verwenden.

In most cases, the overhead of both method calls and casting is small in comparison with the object-allocation times looked at in last month's article. However, these operations will often be used far more frequently than object allocations, so they can still be a significant source of performance problems.

In the remainder of this article, we'll discuss some specific techniques for reducing the need for casting in your code. Specifically, we'll look at how casting often arises from the way subclasses interact with base classes, and explore some techniques for eliminating this type of casting. Next month, in the second part of this look at casting, we'll consider another common cause of casting, the use of generic collections.

Base classes and casting

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }