Ein Fall für die Beibehaltung von Grundelementen in Java

Primitive sind seit ihrer ersten Veröffentlichung im Jahr 1996 Teil der Java-Programmiersprache und bleiben dennoch eine der kontroversesten Sprachfunktionen. John Moore spricht sich stark dafür aus, Primitive in der Java-Sprache zu halten, indem er einfache Java-Benchmarks mit und ohne Primitive vergleicht. Anschließend vergleicht er die Leistung von Java mit der von Scala, C ++ und JavaScript in einem bestimmten Anwendungstyp, bei dem Grundelemente einen bemerkenswerten Unterschied machen.

Frage : Was sind die drei wichtigsten Faktoren beim Immobilienkauf?

Antwort : Standort, Standort, Standort.

Dieses alte und häufig verwendete Sprichwort soll bedeuten, dass der Standort alle anderen Faktoren in Bezug auf Immobilien vollständig dominiert. In einem ähnlichen Argument sind die drei wichtigsten Faktoren, die bei der Verwendung primitiver Typen in Java berücksichtigt werden müssen, Leistung, Leistung, Leistung. Es gibt zwei Unterschiede zwischen dem Argument für Immobilien und dem Argument für Primitive. Erstens dominiert bei Immobilien der Standort in fast allen Situationen, aber die Leistungssteigerungen durch die Verwendung primitiver Typen können von Anwendung zu Anwendung sehr unterschiedlich sein. Zweitens sind bei Immobilien andere Faktoren zu berücksichtigen, obwohl sie im Vergleich zum Standort normalerweise geringfügig sind. Bei primitiven Typen gibt es nur einen Grund, sie zu verwenden - die Leistung;; und dann nur, wenn die Anwendung die Art ist, die von ihrer Verwendung profitieren kann.

Primitive bieten für die meisten Geschäfts- und Internetanwendungen, die ein Client-Server-Programmiermodell mit einer Datenbank im Backend verwenden, nur einen geringen Wert. Die Leistung von Anwendungen, die von numerischen Berechnungen dominiert werden, kann jedoch stark von der Verwendung von Grundelementen profitieren.

Die Aufnahme von Grundelementen in Java war eine der kontroversesten Entscheidungen zum Sprachdesign, wie die Anzahl der Artikel und Forenbeiträge im Zusammenhang mit dieser Entscheidung zeigt. Simon Ritter bemerkte in seiner Grundsatzrede zu JAX London im November 2011, dass ernsthaft über die Entfernung von Grundelementen in einer zukünftigen Version von Java nachgedacht wurde (siehe Folie 41). In diesem Artikel werde ich kurz Primitive und das Dual-Type-System von Java vorstellen. Anhand von Codebeispielen und einfachen Benchmarks werde ich darlegen, warum Java-Grundelemente für bestimmte Arten von Anwendungen benötigt werden. Ich werde auch die Leistung von Java mit der von Scala, C ++ und JavaScript vergleichen.

Messung der Softwareleistung

Die Softwareleistung wird normalerweise in Bezug auf Zeit und Raum gemessen. Die Zeit kann die tatsächliche Laufzeit sein, z. B. 3,7 Minuten, oder die Reihenfolge des Wachstums basierend auf der Größe der Eingabe, z. B. O ( n 2). Ähnliche Kennzahlen gibt es für die Speicherplatzleistung, die häufig in Form der Hauptspeicherauslastung ausgedrückt wird, sich aber auch auf die Festplattenauslastung erstrecken kann. Die Verbesserung der Leistung ist normalerweise mit einem Kompromiss zwischen Zeit und Raum verbunden, da Änderungen zur Verbesserung der Zeit häufig nachteilige Auswirkungen auf den Raum haben und umgekehrt. Eine Messung der Wachstumsreihenfolge hängt vom Algorithmus ab, und der Wechsel von Wrapper-Klassen zu Grundelementen ändert nichts am Ergebnis. Wenn es jedoch um die tatsächliche Zeit- und Raumleistung geht, bietet die Verwendung von Grundelementen anstelle von Wrapper-Klassen gleichzeitig zeitliche und räumliche Verbesserungen.

Primitive versus Objekte

Wie Sie wahrscheinlich bereits wissen, wenn Sie diesen Artikel lesen, verfügt Java über ein System mit zwei Typen, das normalerweise als primitive Typen und Objekttypen bezeichnet wird und häufig einfach als primitive und Objekte abgekürzt wird. In Java sind acht primitive Typen vordefiniert, und ihre Namen sind reservierte Schlüsselwörter. Gebräuchliche Beispiele sind int, doubleund boolean. Im Wesentlichen sind alle anderen Typen in Java, einschließlich aller benutzerdefinierten Typen, Objekttypen. (Ich sage "im Wesentlichen", weil Array-Typen ein bisschen hybride sind, aber sie ähneln eher Objekttypen als primitiven Typen.) Für jeden primitiven Typ gibt es eine entsprechende Wrapper-Klasse, die ein Objekttyp ist. Beispiele sind Integerfür int, Doublefür doubleund Booleanfür boolean.

Primitive Typen sind wertebasiert, aber Objekttypen sind referenzbasiert, und darin liegt sowohl die Kraft als auch die Quelle der Kontroverse primitiver Typen. Betrachten Sie zur Veranschaulichung des Unterschieds die beiden folgenden Erklärungen. Die erste Deklaration verwendet einen primitiven Typ und die zweite eine Wrapper-Klasse.

 int n1 = 100; Integer n2 = new Integer(100); 

Mit Autoboxing, einer Funktion, die JDK 5 hinzugefügt wurde, konnte ich die zweite Deklaration auf einfach verkürzen

 Integer n2 = 100; 

Die zugrunde liegende Semantik ändert sich jedoch nicht. Autoboxing vereinfacht die Verwendung von Wrapper-Klassen und reduziert die Menge an Code, die ein Programmierer schreiben muss, ändert jedoch zur Laufzeit nichts.

Der Unterschied zwischen dem Grundelement n1und dem Wrapper-Objekt n2wird durch das Diagramm in Abbildung 1 veranschaulicht.

John I. Moore, Jr.

Die Variable n1enthält einen ganzzahligen Wert, aber die Variable n2enthält einen Verweis auf ein Objekt, und es ist das Objekt, das den ganzzahligen Wert enthält. Darüber hinaus n2enthält das Objekt, auf das verwiesen wird, auch einen Verweis auf das Klassenobjekt Double.

Das Problem mit Primitiven

Bevor ich versuche, Sie von der Notwendigkeit primitiver Typen zu überzeugen, sollte ich anerkennen, dass viele Menschen mir nicht zustimmen werden. Sherman Alpert in "Primitive Typen, die als schädlich angesehen werden" argumentiert, dass Primitive schädlich sind, weil sie "prozedurale Semantik in ein ansonsten einheitliches objektorientiertes Modell mischen". Primitive sind keine erstklassigen Objekte, existieren jedoch in einer Sprache, die hauptsächlich erstklassige beinhaltet. Klassenobjekte. " Grundelemente und Objekte (in Form von Wrapper-Klassen) bieten zwei Möglichkeiten zur Behandlung logisch ähnlicher Typen, denen jedoch eine sehr unterschiedliche zugrunde liegende Semantik zugrunde liegt. Wie sollten beispielsweise zwei Instanzen auf Gleichheit verglichen werden? Für primitive Typen verwendet man den ==Operator, aber für Objekte ist es die bevorzugte Wahl, den aufzurufenequals()Methode, die für Grundelemente keine Option ist. Ebenso gibt es unterschiedliche Semantiken beim Zuweisen von Werten oder Übergeben von Parametern. Sogar die Standardwerte sind unterschiedlich. zB 0für intversus nullfür Integer.

Weitere Hintergrundinformationen zu diesem Thema finden Sie in Eric Brunos Blog-Beitrag "Eine moderne primitive Diskussion", in dem einige Vor- und Nachteile von Primitiven zusammengefasst sind. Eine Reihe von Diskussionen zum Stapelüberlauf konzentrieren sich auch auf Grundelemente, darunter "Warum verwenden Menschen in Java immer noch Grundelemente?" und "Gibt es einen Grund, immer Objekte anstelle von Grundelementen zu verwenden?" Programmers Stack Exchange veranstaltet eine ähnliche Diskussion mit dem Titel "Wann soll Primitive vs Class in Java verwendet werden?".

Speicherauslastung

A doublein Java belegt immer 64 Bit im Speicher, die Größe einer Referenz hängt jedoch von der Java Virtual Machine (JVM) ab. Auf meinem Computer werden die 64-Bit-Version von Windows 7 und eine 64-Bit-JVM ausgeführt. Daher belegt eine Referenz auf meinem Computer 64 Bit. Basierend auf dem Diagramm in 1 Abbildung würde ich eine einzelne erwarten doublewie n18 Bytes zu besetzen (64 Bits), und ich möchte ein einzelnes erwarten Doublewie n224 Bytes zu besetzen - für die Referenz auf das Objekt 8, 8 für den doubleWert , gespeichert in das Objekt und 8 für den Verweis auf das Klassenobjekt für Double. Außerdem verwendet Java zusätzlichen Speicher, um die Speicherbereinigung für Objekttypen zu unterstützen, nicht jedoch für primitive Typen. Lass es uns überprüfen.

Unter Verwendung eines ähnlichen Ansatzes wie Glen McCluskey in "Java primitive types vs. wrappers" misst die in Listing 1 gezeigte Methode die Anzahl der Bytes, die von einer n-mal-n-Matrix (zweidimensionales Array) von belegt werden double.

Listing 1. Berechnung der Speicherauslastung vom Typ double

 public static long getBytesUsingPrimitives(int n) { System.gc(); // force garbage collection long memStart = Runtime.getRuntime().freeMemory(); double[][] a = new double[n][n]; // put some random values in the matrix for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) a[i][j] = Math.random(); } long memEnd = Runtime.getRuntime().freeMemory(); return memStart - memEnd; } 

Modifying the code in Listing 1 with the obvious type changes (not shown), we can also measure the number of bytes occupied by an n-by-n matrix of Double. When I test these two methods on my computer using 1000-by-1000 matrices, I get the results shown in Table 1 below. As illustrated, the version for primitive type double equates to a little more than 8 bytes per entry in the matrix, roughly what I expected. However, the version for object type Double required a little more than 28 bytes per entry in the matrix. Thus, in this case, the memory utilization of Double is more than three times the memory utilization of double, which should not be a surprise to anyone who understands the memory layout illustrated in Figure 1 above.

Table 1. Memory utilization of double versus Double

Version Total bytes Bytes per entry
Using double 8,380,768 8.381
Using Double 28,166,072 28.166

Runtime performance

To compare the runtime performances for primitives and objects, we need an algorithm dominated by numerical calculations. For this article I have chosen matrix multiplication, and I compute the time required to multiply two 1000-by-1000 matrices. I coded matrix multiplication for double in a straightforward manner as shown in Listing 2 below. While there may be faster ways to implement matrix multiplication (perhaps using concurrency), that point is not really relevant to this article. All I need is common code in two similar methods, one using the primitive double and one using the wrapper class Double. The code for multiplying two matrices of type Double is exactly like that in Listing 2 with the obvious type changes.

Listing 2. Multiplying two matrices of type double

 public static double[][] multiply(double[][] a, double[][] b) { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); int nRows = a.length; int nCols = b[0].length; double[][] result = new double[nRows][nCols]; for (int rowNum = 0; rowNum < nRows; ++rowNum) { for (int colNum = 0; colNum < nCols; ++colNum) { double sum = 0.0; for (int i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; } 

I ran the two methods to multiply two 1000-by-1000 matrices on my computer several times and measured the results. The average times are shown in Table 2. Thus, in this case, the runtime performance of double is more than four times as fast as that of Double. That is simply too much of a difference to ignore.

Table 2. Runtime performance of double versus Double

Version Seconds
Using double 11.31
Using Double 48.48

The SciMark 2.0 benchmark

Bisher habe ich den einfachen Benchmark der Matrixmultiplikation verwendet, um zu demonstrieren, dass Grundelemente eine erheblich höhere Rechenleistung als Objekte liefern können. Um meine Behauptungen zu bekräftigen, werde ich einen wissenschaftlicheren Benchmark verwenden. SciMark 2.0 ist ein Java-Benchmark für wissenschaftliches und numerisches Rechnen, der vom Nationalen Institut für Standards und Technologie (NIST) erhältlich ist. Ich habe den Quellcode für diesen Benchmark heruntergeladen und zwei Versionen erstellt, die Originalversion mit Grundelementen und eine zweite Version mit Wrapper-Klassen. Für die zweite Version ersetzt ich intmit Integerund doublemit Doubledem vollen Effekt der Verwendung von Wrapper - Klassen zu erhalten. Beide Versionen sind im Quellcode für diesen Artikel verfügbar.

Download Benchmarking Java: Laden Sie den Quellcode herunter John I. Moore, Jr.

The SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). Thus, larger numbers are better for this benchmark. Table 3 gives the average composite scores from several runs of each version of this benchmark on my computer. As shown, the runtime performances of the two versions of the SciMark 2.0 benchmark were consistent with the matrix multiplication results above in that the version with primitives was almost five times faster than the version using wrapper classes.

Table 3. Runtime performance of the SciMark benchmark

SciMark version Performance (Mflops)
Using primitives 710.80
Using wrapper classes 143.73

You've seen a few variations of Java programs doing numerical calculations, using both a homegrown benchmark and a more scientific one. But how does Java compare to other languages? I'll conclude with a quick look at how Java's performance compares to that of three other programming languages: Scala, C++, and JavaScript.

Benchmarking Scala

Scala ist eine Programmiersprache, die auf der JVM ausgeführt wird und anscheinend immer beliebter wird. Scala hat ein einheitliches Typensystem, was bedeutet, dass es nicht zwischen Grundelementen und Objekten unterscheidet. Laut Erik Osheim in der numerischen Typklasse von Scala (Teil 1) verwendet Scala nach Möglichkeit primitive Typen, verwendet jedoch bei Bedarf Objekte. In ähnlicher Weise sagt Martin Oderskys Beschreibung der Scala-Arrays: "... ein Scala-Array Array[Int]wird als Java dargestellt int[], ein Array[Double]wird als Java dargestellt double[]..."

Bedeutet dies also, dass das einheitliche Typensystem von Scala eine Laufzeitleistung aufweist, die mit den primitiven Typen von Java vergleichbar ist? Wir werden sehen.