Primitive sind seit ihrer ersten Veröffentlichung im Jahr 1996 Teil der Programmiersprache Java und bleiben dennoch eine der umstritteneren Sprachfunktionen. John Moore spricht sich stark dafür aus, Primitive in der Java-Sprache zu halten, indem er einfache Java-Benchmarks sowohl mit als auch ohne Primitive vergleicht. Er vergleicht dann die Leistung von Java mit der von Scala, C ++ und JavaScript in einem bestimmten Anwendungstyp, wobei Primitive einen bemerkenswerten Unterschied machen.
Frage: Was sind die drei wichtigsten Faktoren beim Immobilienkauf?
Antwort: Ort, Ort, Ort.
Dieses alte und häufig verwendete Sprichwort soll implizieren, dass der Standort alle anderen Faktoren bei Immobilien vollständig dominiert. In einem ähnlichen Argument sind die drei wichtigsten Faktoren für die Verwendung primitiver Typen in Java 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 einer Art von Anwendung zur anderen stark variieren. Zweitens gibt es bei Immobilien andere Faktoren, die zu berücksichtigen sind, obwohl sie im Vergleich zum Standort normalerweise geringfügig sind. Bei primitiven Typen gibt es nur einen Grund, sie zu verwenden — Leistung; und dann nur, wenn die Anwendung die Art ist, die von ihrer Verwendung profitieren kann.
Primitive bieten wenig Wert für die meisten geschäftsbezogenen und Internetanwendungen, die ein Client-Server-Programmiermodell mit einer Datenbank im Backend verwenden. Die Leistung von Anwendungen, die von numerischen Berechnungen dominiert werden, kann jedoch erheblich von der Verwendung von Grundelementen profitieren.
Die Einbeziehung von Primitiven in Java war eine der umstrittensten Entscheidungen für das Sprachdesign, wie die Anzahl der Artikel und Forenbeiträge im Zusammenhang mit dieser Entscheidung zeigt. Simon Ritter bemerkte in seiner Keynote von JAX London im November 2011, dass ernsthaft über die Entfernung von Primitiven in einer zukünftigen Version von Java nachgedacht wurde (siehe Folie 41). In diesem Artikel werde ich kurz Primitive und Javas Dual-Type-System vorstellen. Anhand von Codebeispielen und einfachen Benchmarks werde ich erläutern, 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.
- Primitives versus objects
- Das Problem mit Primitiven
- Speichernutzung
- Auflistung 1. Berechnung der Speichernutzung vom Typ double
- Laufzeitleistung
- Auflistung 2. Multiplizieren von zwei Matrizen vom Typ double
- Der SciMark 2.0 Benchmark
- Benchmarking Scala
- Listing 3. Multiplizieren von zwei Matrizen in Scala
- Benchmarking C ++
- Listing 4. Durch Multiplizieren von zwei Matrizen in C ++
- Benchmarking JavaScript
- Listing 5. Multiplizieren von zwei Matrizen in JavaScript
- Abschließend
- Über den Autor
- Weiterführende Literatur
Primitives versus objects
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, deren Namen reservierte Schlüsselwörter sind. Häufig verwendete Beispiele sind int
, double
und 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 viel mehr Objekttypen als primitiven Typen.) Für jeden primitiven Typ gibt es eine entsprechende Wrapper-Klasse, die ein Objekttyp ist; Beispiele sind Integer
für int
, Double
für double
und Boolean
für boolean
.
Primitive Typen basieren auf Werten, aber Objekttypen basieren auf Referenzen, und darin liegt sowohl die Macht als auch die Quelle der Kontroverse primitiver Typen. Um den Unterschied zu veranschaulichen, betrachten Sie die beiden folgenden Deklarationen. 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 zu JDK 5 hinzugefügt wurde, könnte ich die zweite Deklaration auf einfach
Integer n2 = 100;
verkürzen, aber die zugrunde liegende Semantik ändert sich 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 Primitiv n1
und dem Wrapper-Objekt n2
wird durch das Diagramm in Abbildung 1 veranschaulicht.
Die Variable n1
enthält einen ganzzahligen Wert, aber die Variable n2
enthält einen Verweis auf ein Objekt, und es ist das Objekt, das den ganzzahligen Wert enthält. Darüber hinaus enthält das von n2
referenzierte Objekt 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 Leute mir nicht zustimmen werden. Sherman Alpert in „Primitive Typen als schädlich“ argumentiert, dass Primitive schädlich sind, weil sie „prozedurale Semantik in ein ansonsten einheitliches objektorientiertes Modell mischen. Primitive sind keine erstklassigen Objekte, aber sie existieren in einer Sprache, die in erster Linie erstklassige Objekte beinhaltet.“ Primitive und Objekte (in Form von Wrapper-Klassen) bieten zwei Möglichkeiten, logisch ähnliche Typen zu behandeln, aber sie haben eine sehr unterschiedliche zugrunde liegende Semantik. Wie sollen beispielsweise zwei Instanzen auf Gleichheit verglichen werden? Für primitive Typen verwendet man den Operator ==
, aber für Objekte ist die bevorzugte Wahl, die Methode equals()
aufzurufen, die für Primitive keine Option ist. Ebenso gibt es unterschiedliche Semantik beim Zuweisen von Werten oder Übergeben von Parametern. Sogar die Standardwerte sind unterschiedlich, z. B. 0
für int
gegenüber null
für Integer
.
Weitere Hintergründe zu diesem Thema finden Sie in Eric Brunos Blogbeitrag „A modern primitive discussion“, der einige der Vor- und Nachteile von Primitiven zusammenfasst. Eine Reihe von Diskussionen über Stack Overflow konzentrieren sich auch auf Primitive, einschließlich „Warum verwenden Leute immer noch primitive Typen in Java?“ und „Gibt es einen Grund, immer Objekte anstelle von Grundelementen zu verwenden?.“ Programmers Stack Exchange veranstaltet eine ähnliche Diskussion mit dem Titel „Wann primitive vs-Klasse in Java verwenden?“.
Speichernutzung
A double
belegt in Java immer 64 Bit im Speicher, aber die Größe einer Referenz hängt von der Java Virtual Machine (JVM) ab. Auf meinem Computer wird die 64-Bit-Version von Windows 7 und eine 64-Bit-JVM ausgeführt, und daher belegt eine Referenz auf meinem Computer 64 Bit. Basierend auf dem Diagramm in Abbildung 1 würde ich erwarten, dass ein einzelnes double
wie n1
8 Bytes (64 Bit) belegt, und ich würde erwarten, dass ein einzelnes Double
wie n2
24 Bytes belegt — 8 für den Verweis auf das Objekt, 8 für den im Objekt gespeicherten double
-Wert 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.
Mit einem Ansatz ähnlich dem von Glen McCluskey in „Java primitive types vs. wrapper,“die in Listing 1 gezeigte Methode misst die Anzahl der Bytes, die von einer n-mal-n-Matrix (zweidimensionales Array) von double
belegt werden.
Auflistung 1. Berechnung der Speichernutzung vom Typ double
Durch Ändern des Codes in Listing 1 mit den offensichtlichen Typänderungen (nicht gezeigt) können wir auch die Anzahl der Bytes messen, die von einer n-mal-n-Matrix von Double
belegt werden. Wenn ich diese beiden Methoden auf meinem Computer mit 1000 x 1000 Matrizen teste, erhalte ich die Ergebnisse in Tabelle 1 unten. Wie dargestellt, entspricht die Version für den primitiven Typ double
etwas mehr als 8 Byte pro Eintrag in der Matrix, ungefähr dem, was ich erwartet hatte. Die Version für den Objekttyp Double
benötigte jedoch etwas mehr als 28 Byte pro Eintrag in der Matrix. Somit ist in diesem Fall die Speichernutzung von Double
mehr als dreimal so groß wie die Speichernutzung von double
, was für jeden, der das in Abbildung 1 dargestellte Speicherlayout versteht, keine Überraschung sein sollte.
Laufzeitleistung
Um die Laufzeitleistung für Primitive und Objekte zu vergleichen, benötigen wir einen Algorithmus, der von numerischen Berechnungen dominiert wird. Für diesen Artikel habe ich die Matrixmultiplikation ausgewählt und berechne die Zeit, die erforderlich ist, um zwei 1000 x 1000 Matrizen zu multiplizieren. Ich habe die Matrixmultiplikation für double
auf einfache Weise codiert, wie in Liste 2 unten gezeigt. Obwohl es schnellere Möglichkeiten gibt, die Matrixmultiplikation zu implementieren (möglicherweise mithilfe von Parallelität), ist dieser Punkt für diesen Artikel nicht wirklich relevant. Alles, was ich brauche, ist allgemeiner Code in zwei ähnlichen Methoden, eine mit dem Primitiv double
und eine mit der Wrapper-Klasse Double
. Der Code zum Multiplizieren von zwei Matrizen vom Typ Double
ist genau wie in Auflistung 2 mit den offensichtlichen Typänderungen.
Auflistung 2. Multiplizieren von zwei Matrizen vom Typ double
Ich habe die beiden Methoden zum Multiplizieren von zwei 1000 x 1000 Matrizen auf meinem Computer mehrmals ausgeführt und die Ergebnisse gemessen. Die durchschnittlichen Zeiten sind in Tabelle 2 angegeben. Somit ist in diesem Fall die Laufzeitleistung von double
mehr als viermal so schnell wie die von Double
. Das ist einfach ein zu großer Unterschied, um ihn zu ignorieren.
Der SciMark 2.0 Benchmark
Bisher habe ich den einzelnen, einfachen Benchmark der Matrixmultiplikation verwendet, um zu demonstrieren, dass Primitive eine signifikant höhere Rechenleistung erbringen können als Objekte. Um meine Behauptungen zu untermauern, werde ich einen wissenschaftlicheren Maßstab verwenden. SciMark 2.0 ist ein Java-Benchmark für wissenschaftliches und numerisches Rechnen, der vom National Institute of Standards and Technology (NIST) zur Verfügung gestellt wird. Ich habe den Quellcode für diesen Benchmark heruntergeladen und zwei Versionen erstellt, die Originalversion mit Primitiven und eine zweite Version mit Wrapper-Klassen. Für die zweite Version habe ich int
durch Integer
und double
durch Double
ersetzt, um die volle Wirkung der Verwendung von Wrapper-Klassen zu erzielen. Beide Versionen sind im Quellcode für diesen Artikel verfügbar.
Der SciMark-Benchmark misst die Leistung mehrerer Rechenroutinen und meldet eine zusammengesetzte Punktzahl in ungefähren Mflops (Millionen von Gleitkommaoperationen pro Sekunde). Daher sind größere Zahlen für diesen Benchmark besser. Tabelle 3 gibt die durchschnittlichen zusammengesetzten Werte aus mehreren Läufen jeder Version dieses Benchmarks auf meinem Computer an. Wie gezeigt, stimmten die Laufzeitleistungen der beiden Versionen des SciMark 2.0-Benchmarks mit den obigen Ergebnissen der Matrixmultiplikation überein, da die Version mit Primitiven fast fünfmal schneller war als die Version mit Wrapper-Klassen.
Sie haben einige Variationen von Java-Programmen gesehen, die numerische Berechnungen durchführen und sowohl einen selbst entwickelten als auch einen wissenschaftlicheren Benchmark verwenden. Wie unterscheidet sich Java von anderen Sprachen? Abschließend möchte ich einen kurzen Blick darauf werfen, wie sich die Leistung von Java mit der von drei anderen Programmiersprachen vergleicht: Scala, C ++ und JavaScript.
Benchmarking Scala
Scala ist eine Programmiersprache, die auf der JVM läuft und an Popularität zu gewinnen scheint. Scala verfügt über ein einheitliches Typsystem, dh es wird nicht zwischen Grundelementen und Objekten unterschieden. Laut Erik Osheim in Scalas numerischer Typklasse (Pt. 1) verwendet Scala nach Möglichkeit primitive Typen, verwendet jedoch bei Bedarf Objekte. In ähnlicher Weise sagt Martin Oderskys Beschreibung von Scalas Arrays: „… ein Scala-Array Array
wird als Java int
dargestellt, ein Array
wird als Java double
dargestellt…“
Bedeutet dies also, dass das einheitliche Typsystem von Scala eine Laufzeitleistung aufweist, die mit den primitiven Typen von Java vergleichbar ist? Mal sehen.
Ich bin mit Scala nicht so gut vertraut wie mit Java, aber ich habe versucht, den Code für den Matrixmultiplikations-Benchmark direkt von Java nach Scala zu konvertieren. Das Ergebnis ist in Listing 3 unten dargestellt. Als ich die Scala-Version des Benchmarks auf meinem Computer ausführte, betrug sie durchschnittlich 12,30 Sekunden, was die Leistung von Scala der von Java mit Primitiven sehr nahe bringt. Dieses Ergebnis ist viel besser als erwartet und unterstützt die Behauptungen darüber, wie Scala mit numerischen Typen umgeht.
Listing 3. Multiplizieren von zwei Matrizen in Scala
Benchmarking C ++
Da C ++ direkt auf „Bare Metal“ und nicht in einer virtuellen Maschine ausgeführt wird, würde man natürlich erwarten, dass C ++ schneller läuft als Java. Darüber hinaus wird die Java-Leistung geringfügig dadurch verringert, dass Java den Zugriff auf Arrays überprüft, um sicherzustellen, dass jeder Index innerhalb der für das Array deklarierten Grenzen liegt, während C ++ dies nicht tut (eine C ++ – Funktion, die zu Pufferüberläufen führen kann, die von Hackern ausgenutzt werden können). Ich fand C ++ etwas umständlicher als Java im Umgang mit grundlegenden zweidimensionalen Arrays, aber glücklicherweise kann ein Großteil dieser Unbeholfenheit in den privaten Teilen einer Klasse verborgen sein. Für C ++ habe ich eine einfache Version einer Matrix
-Klasse erstellt und den Operator *
zum Multiplizieren von zwei Matrizen überladen, aber der grundlegende Matrixmultiplikationsalgorithmus wurde direkt aus der Java-Version konvertiert. Der C ++ – Quellcode wird in Listing 4 angezeigt.
Listing 4. Durch Multiplizieren von zwei Matrizen in C ++
Mit Eclipse CDT (Eclipse für C ++ – Entwickler) mit dem MinGW C ++ – Compiler können sowohl Debug- als auch Release-Versionen einer Anwendung erstellt werden. Um C ++ zu testen, habe ich die Release-Version mehrmals ausgeführt und die Ergebnisse gemittelt. Wie erwartet lief C ++ bei diesem einfachen Benchmark mit durchschnittlich 7,58 Sekunden auf meinem Computer merklich schneller. Wenn die rohe Leistung der primäre Faktor für die Auswahl einer Programmiersprache ist, dann ist C ++ die Sprache der Wahl für numerisch intensive Anwendungen.
Benchmarking JavaScript
Okay, das hat mich überrascht. Da JavaScript eine sehr dynamische Sprache ist, habe ich erwartet, dass seine Leistung die schlechteste von allen ist, noch schlimmer als Java mit Wrapper-Klassen. Tatsächlich war die Leistung von JavaScript jedoch viel näher an der von Java mit Primitiven. Um JavaScript zu testen, habe ich Node installiert.js, eine JavaScript-Engine mit dem Ruf, sehr effizient zu sein. Das Ergebnis betrug durchschnittlich 15,91 Sekunden. Listing 5 zeigt die JavaScript-Version des Matrix-Multiplikations-Benchmarks, den ich auf Node ausgeführt habe.js
Listing 5. Multiplizieren von zwei Matrizen in JavaScript
Abschließend
Als Java vor etwa 18 Jahren zum ersten Mal auf den Markt kam, war es aus Leistungsperspektive nicht die beste Sprache für Anwendungen, die von numerischen Berechnungen dominiert werden. Aber im Laufe der Zeit, mit technologischen Fortschritten in Bereichen wie Just-in-Time (JIT) -Kompilierung (auch bekannt als adaptive oder dynamische Kompilierung), ist Javas Leistung für diese Art von Anwendungen jetzt vergleichbar mit der von Sprachen, die in nativen Code kompiliert werden, wenn primitive Typen verwendet werden.
Darüber hinaus machen Primitive die Garbage Collection überflüssig und bieten damit einen weiteren Leistungsvorteil von Primitiven gegenüber Objekttypen. Tabelle 4 fasst die Laufzeitleistung des Matrixmultiplikations-Benchmarks auf meinem Computer zusammen. Andere Faktoren wie Wartbarkeit, Portabilität und Entwickler-Know-how machen Java zu einer besseren Wahl für viele solcher Anwendungen.
Wie bereits erwähnt, scheint Oracle ernsthaft über die Entfernung von Primitiven in einer zukünftigen Version von Java nachzudenken. Wenn der Java-Compiler keinen Code mit einer Leistung generieren kann, die mit der von Primitiven vergleichbar ist, würde ihre Entfernung aus Java die Verwendung von Java für bestimmte Anwendungsklassen ausschließen. nämlich jene Anwendungen, die von numerischen Berechnungen dominiert werden. In diesem Artikel habe ich einen einfachen Benchmark verwendet, der auf Matrixmultiplikation basiert, und einen wissenschaftlicheren Benchmark, SciMark 2.0, um diesen Punkt zu argumentieren.
Über den Autor
John I. Moore, Jr., Professor für Mathematik und Informatik an der Citadel, verfügt über ein breites Spektrum an Erfahrungen in Industrie und Wissenschaft mit spezifischer Expertise in den Bereichen objektorientierte Technologie, Software Engineering und angewandte Mathematik. Seit mehr als drei Jahrzehnten entwirft und entwickelt er Software mit relationalen Datenbanken und mehreren Hochsprachen und arbeitet seit Version 1.1 intensiv in Java. Darüber hinaus hat er zahlreiche akademische Kurse und Industrieseminare zu fortgeschrittenen Themen der Informatik entwickelt und unterrichtet.
Weiterführende Literatur
- Paul Krill schrieb in „Oracle lays out long-range Java intentions“ (JavaWorld, März 2012) über Oracles langfristige Pläne für Java. Dieser Artikel, zusammen mit dem zugehörigen Kommentar-Thread, motivierte mich, diese Verteidigung der Primitiven zu schreiben.
- Szymon Guz schreibt über seine Ergebnisse im Benchmarking primitiver Typen und Wrapper-Klassen in „Primitives and objects benchmark in Java“ (SimonOnSoftware, Januar 2011).
- Auf der Support—Website für Programming – Principles and Practice Using C++ (Addison-Wesley, 2009) stellt der C ++ – Entwickler Bjarne Stroustrup eine Implementierung für eine Matrixklasse bereit, die viel vollständiger ist als die diesem Artikel beigefügte.
- John Rose, Brian Goetz und Guy Steele diskutieren ein Konzept namens Werttypen in „State of the Values“ (OpenJDK.net , April 2014). Werttypen können als unveränderliche benutzerdefinierte Aggregattypen ohne Identität betrachtet werden, indem Eigenschaften von Objekten und Grundelementen kombiniert werden. Das Mantra für Werttypen lautet: „Codes wie eine Klasse, funktioniert wie ein int.“