From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001
From: Sven Eisenhauer
+In Java gibt es zwei unterschiedliche Klassen String
+und StringBuilder
+zur Verarbeitung von Zeichenketten, deren prinzipielle Eigenschaften
+in Kapitel 11 erläutert
+wurden. Java-Anfänger verwenden meist hauptsächlich die
+Klasse String,
+denn sie stellt die meisten Methoden zur Zeichenkettenextraktion und
+-verarbeitung zur Verfügung und bietet mit dem +-Operator eine
+bequeme Möglichkeit, Zeichenketten miteinander zu verketten.
+
+
+Daß diese Bequemlichkeit ihren Preis hat, zeigt folgender Programmausschnitt:
+
+
+
+
+
+
+ Titel
+ Inhalt
+ Suchen
+ Index
+ DOC
+ Handbuch der Java-Programmierung, 5. Auflage
+
+ <<
+ <
+ >
+ >>
+ API
+ Kapitel 50 - Performance-Tuning
+
+
+
+
+
+50.2 Tuning-Tipps
+
+
+
+
+
+
+
+
+50.2.1 String und StringBuilder
+
+
+
+
+String-Verkettung
+
+
+
+
+Listing 50.1: Langsame String-Verkettung
+
+
+
+
+
+001 String s;
+002 s = "";
+003 for (int i = 0; i < 20000; ++i) {
+004 s += "x";
+005 }
+
+
+Das Programmfragment hat die Aufgabe, einen String zu erstellen, der +aus 20000 aneinandergereihten »x« besteht. Das ist zwar +nicht sehr praxisnah, illustriert aber die häufig vorkommende +Verwendung des +=-Operators auf Strings. Der obige Code ist sehr ineffizient, +denn er läuft langsam und belastet das Laufzeitsystem durch 60000 +temporäre Objekte, die alloziert und vom Garbage Collector wieder +freigegeben werden müssen. Der Compiler übersetzt das Programmfragment +etwa so: + + +
+
+
+
+001 String s;
+002 s = "";
+003 for (int i = 0; i < 20000; ++i) {
+004 s = new StringBuilder(s).append("x").toString();
+005 }
+
+ |
+
+Dieser Code ist in mehrfacher Hinsicht unglücklich. Pro Schleifendurchlauf +wird ein temporäres StringBuilder-Objekt +alloziert und mit dem zuvor erzeugten String initialisiert. Der Konstruktor +von StringBuilder +erzeugt ein internes Array (also eine weitere Objektinstanz), um die +Zeichenkette zu speichern. Immerhin ist dieses Array 16 Byte größer +als eigentlich erforderlich, so dass der nachfolgende Aufruf von append +das Array nicht neu allozieren und die Zeichen umkopieren muss. Schließlich +wird durch den Aufruf von toString +ein neues String-Objekt +erzeugt und s zugewiesen. Auf +diese Weise werden pro Schleifendurchlauf drei temporäre Objekte +erzeugt, und der Code ist durch das wiederholte Kopieren der Zeichen +im Konstruktor von StringBuilder +sehr ineffizient. + +
+Eine eminente Verbesserung ergibt sich, wenn die Klasse StringBuilder +und ihre Methode append +direkt verwendet werden: + + +
+
+
+
+001 String s;
+002 StringBuilder sb = new StringBuilder(1000);
+003 for (int i = 0; i < 20000; ++i) {
+004 sb.append("x");
+005 }
+006 s = sb.toString();
+
+ |
+
+Hier wird zunächst ein StringBuilder +erzeugt und mit einem 1000 Zeichen großen Puffer versehen. Da +die StringBuilder-Klasse +sich die Länge der gespeicherten Zeichenkette merkt, kann der +Aufruf append("x") meist in +konstanter Laufzeit erfolgen. Dabei ist ein Umkopieren nur dann erforderlich, +wenn der interne Puffer nicht mehr genügend Platz bietet, um +die an append +übergebenen Daten zu übernehmen. In diesem Fall wird ein +größeres Array alloziert und der Inhalt des bisherigen +Puffers umkopiert. In der Summe ist die letzte Version etwa um den +Faktor 10 schneller als die ersten beiden und erzeugt 60000 temporäre +Objekte weniger. + +
+Interessant ist dabei der Umfang der Puffervergrößerung, +den das StringBuilder-Objekt +vornimmt, denn er bestimmt, wann bei fortgesetztem Aufruf von append +das nächste Mal umkopiert werden muss. Anders als beispielsweise +bei der Klasse Vector, +die einen veränderbaren Ladefaktor +besitzt, verdoppelt sich die Größe eines StringBuilder-Objekts +bei jeder Kapazitätserweiterung. Dadurch wird zwar möglicherweise +mehr Speicher als nötig alloziert, aber die Anzahl der Kopiervorgänge +wächst höchstens logarithmisch mit der Gesamtmenge der eingefügten +Daten. In unserem Beispiel kann der interne Puffer zunächst 1000 +Zeichen aufnehmen, wird beim nächsten Überlauf auf etwa +2000 Zeichen vergrößert, dann auf 4000, 8000, 16000 und +schließlich auf 32000 Zeichen. Hätten wir die initiale +Größe auf 20000 Zeichen gesetzt, wäre sogar überhaupt +kein Kopiervorgang erforderlich geworden und das Programm hätte +12000 Zeichen weniger alloziert. +
+
![]() |
+![]() |
+
+
+ +Bei der Verwendung der Operatoren + und += auf String-Objekten +sollte man zusätzlich bedenken, dass deren Laufzeit nicht konstant +ist (bzw. ausschließlich von der Länge des anzuhängenden +Strings abhängt). Tatsächlich hängt sie auch stark +von der Länge des Strings ab, an den angehängt werden soll, +denn die Laufzeit eines Kopiervorgangs wächst nun einmal proportional +zur Länge des zu kopierenden Objekts. Damit wächst das Laufzeitverhalten +der Schleife in Listing 50.1 +nicht linear, sondern annähernd quadratisch. Es verschlechtert +sich also mit zunehmender Länge der Schleife überproportional. |
+
+
|
+![]() |
+
+Ein immer noch deutlicher, wenn auch nicht ganz so drastischer Vorteil +bei der Verwendung von StringBuilder +ergibt sich beim Einfügen von Zeichen am vorderen Ende +des Strings: + + +
+
+
+
+001 String s;
+002 s = "";
+003 for (int i = 0; i < 10000; ++i) {
+004 s = "x" + s;
+005 }
+
+ |
+
+In diesem Beispiel wird wiederholt ein Zeichen vorne in den String +eingefügt. Der Compiler wandelt das Programm auch hier in wiederholte +Aufrufe von StringBuilder-Methoden +um, wobei unnötig viele Zwischenobjekte entstehen und unnötig +oft kopiert werden muss. Eine bessere Lösung kann man auch hier +durch direkte Verwendung eines StringBuilder-Objekts +erzielen: + + +
+
+
+
+001 String s;
+002 StringBuilder sb = new StringBuilder(1000);
+003 for (int i = 0; i < 10000; ++i) {
+004 sb.insert(0, "x");
+005 }
+006 s = sb.toString();
+
+ |
+
+Im Test war die Laufzeit dieser Variante etwa um den Faktor vier besser +als die der ersten Version. Außerdem wird nicht ein einziges +temporäres Objekt erzeugt, so dass zusätzlich das Memory-Subsystem +und der Garbage Collector entlastet werden. +
+
![]() |
+![]() |
+
+
+
+Seit dem JDK 1.2 gibt es in der Klasse StringBuilder
+(beziehungsweise in der Klasse StringBuffer)
+eine Methode delete,
+mit der ein Teil der Zeichenkette gelöscht werden kann. Dadurch
+können beispielsweise Programmteile der folgenden Art beschleunigt
+werden:
+
+
+Anstatt hier die ersten 1000 Zeichen mit allen Zeichen ab Position
+2000 zu verbinden, kann unter Verwendung eines StringBuilders
+auch direkt das gewünschte Stück gelöscht werden:
+
+ |
+
+
|
+![]() |
+
+Den vorangegangenen Abschnitten kann man entnehmen, dass die Verwendung +der Klasse StringBuilder +meist dann sinnvoll ist, wenn die Zeichenkette zunächst aus vielen +kleinen Teilen aufgebaut werden soll oder wenn sie sich häufig +ändert. Ist der String dagegen fertig konstruiert oder muss auf +einen vorhandenen String lesend zugegriffen werden, geht dies im allgemeinen +mit den vielseitigeren Methoden der Klasse String +besser. Um einen StringBuilder +in einen String +zu konvertieren, wird die Methode toString +aufgerufen, die durch einen kleinen Trick sehr effizient arbeitet. +Anstatt beim Aufruf von toString +einen Kopiervorgang zu starten, teilen sich String- +und StringBuilder-Objekt +nach dem Aufruf das interne Zeichenarray, d.h. beide Objekte verwenden +ein- und denselben Puffer. Normalerweise wäre diese Vorgehensweise +indiskutabel, denn nach der nächsten Änderung des StringBuilder-Objekts +hätte sich dann auch der Inhalt des String-Objekts +verändert (was per Definition nicht erlaubt ist). + +
+Um das zu verhindern, wird vom Konstruktor der String-Klasse +während des Aufrufs von toString +ein shared-Flag im StringBuilder-Objekt +gesetzt. Dieses wird bei allen verändernden StringBuilder-Methoden +abgefragt und führt dazu, dass - wenn es gesetzt ist - der Pufferinhalt +vor der Veränderung kopiert und die Änderung auf der Kopie +vorgenommen wird. Ein echter Kopiervorgang wird also solange nicht +erforderlich, wie auf den StringBuilder +nicht schreibend zugegriffen wird. + + + + +
+Da die Klasse String +keine Möglichkeit bietet, die gespeicherte Zeichenkette nach +der Instanzierung des Objekts zu verändern, können einige +Operationen auf Zeichenketten sehr effizient implementiert werden. +So erfordert beispielsweise die einfache Zuweisung zweier String-Objekte +lediglich das Kopieren eines Zeigers, ohne dass durch Aliasing +die Gefahr besteht, beim Ändern eines Strings versehentlich weitere +Objekte zu ändern, die auf denselben Speicherbereich zeigen. + +
+Soll ein String
+physikalisch kopiert werden, kann das mit Hilfe eines speziellen Konstruktors
+erreicht werden:
+
+
+String s2 = new String(s1);
+
+
+
+
+Da der interne Puffer hierbei kopiert wird, ist der Aufruf natürlich +ineffizienter als die einfache Zuweisung. + +
+Auch die Methode substring +der Klasse String +konnte sehr effizient implementiert werden. Sie erzeugt zwar ein neues +String-Objekt, +aber den internen Zeichenpuffer teilt es sich mit dem bisherigen Objekt. +Lediglich die Membervariablen, in denen die Startposition und relevante +Länge des Puffers festgehalten werden, müssen im neuen Objekt +angepasst werden. Dadurch ist auch das Extrahieren von langen Teilzeichenketten +recht performant. Dasselbe gilt für die Methode trim, +die ebenfalls substring +verwendet und daher keine Zeichen kopieren muss. + + + + +
+Soll ein String +durchlaufen werden, so kann mit der Methode length +seine Länge ermittelt werden, und durch wiederholten Aufruf von +charAt +können alle Zeichen nacheinander abgeholt werden. Alternativ +könnte man auch zunächst ein Zeichenarray allozieren und +durch Aufruf von getChars +alle Zeichen hineinkopieren. Beim späteren Durchlaufen wäre +dann kein Methodenaufruf mehr erforderlich, sondern die einzelnen +Array-Elemente könnten direkt verwendet werden. Die Laufzeitunterschiede +zwischen beiden Varianten sind allerdings minimal und werden in der +Praxis kaum ins Gewicht fallen (da die Klasse String +als final +deklariert wurde und die Methode charAt +nicht synchronized +ist, kann sie sehr performant aufgerufen werden). + + + + +
+Wenn Sie eine Zeichenkette aus Einzelstücken zusammensetzen verwenden +Sie am Besten die Klasse StringBuilder. +Doch wenn Sie diese Zeichenkette anschließend als Parameter +oder Rückgabewert übergeben, wird dieser häufig über +die Methode toString in einen +äquivalenten String umgewandelt. +Wird die Zeichenkette anschließend weiter bearbeitet, wird der +übergebene String anschließend +wieder in einen StringBuilder +umgewandelt und so weiter. + +
+Sie können sich diese unnötigen Kopieroperationen allerdings +auch sparen und Ihren Code gleichzeitig wesentlich lesbarer machen, +indem Sie in diesen Fällen einfach in der Methodensignatur einen +Parameter vom Typ StringBuilder +definieren und das Objekt direkt übergeben. + +
+Können Sie die Signatur der Methode allerdings nicht ändern +- etwa, weil die Methode auch mit gewöhnlichen Strings aufgerufen +werden soll, hält das JDK seit der Version 5 das Interface CharSequence +für Sie bereit, welches bereits in Abschnitt 11.5 +vorgestellt wurde. Dieses Interface wird sowohl von der Klasse String +als auch von StringBuilder implementiert +und gestattet es Objekte beiden Typs zu übergeben. + + + + +
+Eine der häufigsten Operationen in objektorientierten Programmiersprachen +ist der Aufruf einer Methode an einer Klasse oder einem Objekt. Generell +werden Methodenaufrufe in Java recht performant ausgeführt. Ihr +Laufzeitverhalten ist jedoch stark von ihrer Signatur und ihren Attributen +abhängig. Tabelle 50.1 +gibt einen Überblick über die Laufzeit (in msec.) von 10 +Millionen Aufrufen einer trivialen Methode unter unterschiedlichen +Bedingungen. Alle Messungen wurden mit dem JDK 1.2 Beta 4 auf einem +PentiumII-266 unter Windows 95 vorgenommen. + +
+
| Signatur/Attribute | +Ohne JIT | +Mit JIT |
| public | +5650 | +280 |
| public, mit 4 Parametern | +7800 | +390 |
| public static | +5060 | +110 |
| protected | +5770 | +280 |
| private | +5820 | +50 |
| public synchronized | +9500 | +4660 |
| public final | +6260 | +50 |
+Tabelle 50.1: Geschwindigkeit von Methodenaufrufen
+ ++Dabei fallen einige Dinge auf: +
+Weiterhin ist zu beachten, dass der polymorphe Aufruf von Methoden +Zeit kostet (was nicht aus dieser Tabelle abgelesen werden kann). +Ist beispielsweise aus einer Klasse A eine weitere Klasse B +abgeleitet, so ist der Aufruf von Methoden auf einem Objekt des Typs +A kostspieliger als der auf einem Objekt des Typs B. + +
+Aus diesen Ergebnissen allgemeingültige Empfehlungen abzuleiten, +ist schwierig. Zwar empfiehlt es sich offensichtlich, Methoden als +private +bzw. final +zu deklarieren, wenn sicher ist, dass sie in abgeleiteten Klassen +nicht aufgerufen bzw. überlagert werden sollen. Auch könnte +man versuchen, verstärkt Klassenmethoden zu verwenden oder zur +Vermeidung von polymorphen Aufrufen die Vererbungshierachie zu beschränken +oder mit Hilfe des Attributs final +ganz abzuschneiden. All diese Entscheidungen hätten aber einen +starken Einfluss auf das Klassendesign der Anwendung und könnten +sich leicht an anderer Stelle als Sackgasse herausstellen. + +
+Der einzig wirklich allgemeingültige Rat besteht darin, Methoden +nur dann als synchronized +zu deklarieren, wenn es wirklich erforderlich ist. Eine Methode, die +keine Membervariablen verwendet, die gleichzeitig von anderen Threads +manipuliert werden, braucht auch nicht synchronisiert zu werden. Eine +Anwendung, die nur einen einzigen Thread besitzt und deren Methoden +nicht von Hintergrundthreads aufgerufen werden, braucht überhaupt +keine synchronisierten Methoden in eigenen Klassen. + + + + +
+Ein Vector +ist ein bequemes Hilfsmittel, um Listen von Objekten zu speichern, +auf die sowohl sequenziell als auch wahlfrei zugriffen werden kann. +Aufgrund seiner einfachen Anwendung und seiner Flexibilität bezüglich +der Art und Menge der zu speichernden Elemente wird er in vielen Programmen +ausgiebig verwendet. Bei falschem Einsatz können Vektoren durchaus +zum Performance-Killer werden, und wir wollen daher einige Hinweise +zu ihrer Verwendung geben. + +
+Zunächst einmal ist der Datenpuffer eines Vektors als Array implementiert. +Da die Größe von Arrays nach ihrer Initialisierung nicht +mehr verändert werden kann, erfordert das Einfügen neuer +Elemente möglicherweise das Allozieren eines neuen Puffers und +das Umkopieren der vorhandenen Elemente. Ein Vector +besitzt dazu die beiden Attribute Kapazität und Ladefaktor. +Die Kapazität gibt an, wie viele Elemente insgesamt aufgenommen +werden können, also wie groß der interne Puffer ist. Der +Ladefaktor bestimmt, um wie viele Elemente der interne Puffer erweitert +wird, wenn beim Einfügen eines neuen Elements nicht mehr ausreichend +Platz vorhanden ist. Je kleiner die anfängliche Kapazität +und der Ladefaktor sind, desto häufiger ist beim fortgesetzten +Einfügen von Elementen ein zeitaufwändiges Umkopieren erforderlich. + +
+Wird ein Vector
+ohne Argumente instanziert, so hat sein Puffer eine anfängliche
+Kapazität von 10 Objekten und der Ladefaktor ist 0. Letzteres
+bedeutet, dass die Kapazität bei jeder Erweiterung verdoppelt
+wird (analog zur Klasse StringBuilder,
+s. Abschnitt 50.2.1). Alternativ
+kann die Kapazität oder auch beide Werte beim Instanzieren an
+den Konstruktor übergeben werden. Durch die folgende Deklaration
+wird ein Vector
+mit einer anfänglichen Kapazität von 100 Elementen und einem
+Ladefaktor von 50 angelegt:
+
+
+Vector v = new Vector(100, 50);
+
+
+
+
+Ein weiteres Problem der Klasse Vector +ist, dass die meisten ihrer Methoden als synchronized +deklariert wurden. Dadurch kann ein Vector +zwar sehr einfach als gemeinsame Datenstruktur mehrerer Threads verwendet +werden. Die Zugriffsmethoden sind aber leider auch ohne Multi-Threading-Betrieb +entsprechend langsam. +
+
![]() |
+![]() |
+
+
+ +Seit der Version 1.2 des JDK stehen mit den Klassen LinkedList +und ArrayList +auch alternative Listenimplementierungen zur Verfügung, die anstelle +von Vector +verwendet werden können. Hier ist jedoch Vorsicht geboten, soll +das Programm nicht langsamer laufen als vorher. Die Klasse LinkedList +implementiert die Datenstruktur in klassischer Form als doppelt verkettete +Liste ihrer Elemente. Zwar entfallen dadurch die Kopiervorgänge, +die beim Erweitern des Arrays erforderlich waren. Durch die Vielzahl +der allozierten Objekte, in denen die Listenelemente und die Zeiger +gespeichert werden müssen, und die teilweise ineffiziente Implementierung +einiger Grundoperationen (insbesondere add) +hat sich LinkedList +jedoch im Test als relativ ineffizient herausgestellt. Wesentlich +bessere Ergebnisse gab es mit der Klasse ArrayList. +Sie ist ähnlich wie Vector +implementiert, verzichtet aber (wie die meisten 1.2er Collections) +auf die synchronized-Attribute +und ist daher - insbesondere bei aktiviertem JIT und Zugriff mit add +und get +sehr - performant. |
+
+
|
+![]() |
+
+Listing 50.6 zeigt drei +Methoden, die jeweils ein String-Array übergeben bekommen und +daraus eine bestimmte Anzahl von Elementen zurückgeben. Die erste +Version verwendet einen Vector, +die zweite eine LinkedList +und die dritte eine ArrayList +zur Datenspeicherung. Im Test war die dritte Version eindeutig die +schnellste. Bei aktiviertem JIT und Übergabe von 100000 Elementen, +von denen jeweils die Hälfte zurückgegeben wurden, war das +Verhältnis der Laufzeiten der drei Methoden etwa 3:18:1. + + +
+
+
+
+001 public static String[] vtest1(String el[], int retsize)
+002 {
+003 //Verwendet Vector
+004 Vector v = new Vector(el.length + 10);
+005 for (int i = 0; i < el.length; ++i) {
+006 v.addElement(el[i]);
+007 }
+008 String[] ret = new String[retsize];
+009 for (int i = 0; i < retsize; ++i) {
+010 ret[i] = (String)v.elementAt(i);
+011 }
+012 return ret;
+013 }
+014
+015 public static String[] vtest2(String el[], int retsize)
+016 {
+017 //Verwendet LinkedList
+018 LinkedList l = new LinkedList();
+019 for (int i = 0; i < el.length; ++i) {
+020 l.add(el[i]);
+021 }
+022 String[] ret = new String[retsize];
+023 Iterator it = l.iterator();
+024 for (int i = 0; i < retsize; ++i) {
+025 ret[i] = (String)it.next();
+026 }
+027 return ret;
+028 }
+029
+030 public static String[] vtest3(String el[], int retsize)
+031 {
+032 //Verwendet ArrayList
+033 ArrayList l = new ArrayList(el.length + 10);
+034 for (int i = 0; i < el.length; ++i) {
+035 l.add(el[i]);
+036 }
+037 String[] ret = new String[retsize];
+038 for (int i = 0; i < retsize; ++i) {
+039 ret[i] = (String)l.get(i);
+040 }
+041 return ret;
+042 }
+
+ |
+
+Ist es dagegen erforderlich, viele Einfügungen und Löschungen +innerhalb der Liste vorzunehmen, sollte im allgemeinen eine zeigerbasierte +Implementierung der arraybasierten vorgezogen werden. Während +es bei letzterer stets erforderlich ist, einen Teil des Arrays umzukopieren, +wenn ein Element eingefügt oder gelöscht wird, brauchen +bei den verzeigerten Datenstrukturen lediglich ein paar Verweise aktualisiert +zu werden. + + + + +
+Seit dem JDK 1.1 gibt es die Writer-Klassen, +mit denen Character-Streams verarbeitet +werden können. Passend zur internen Darstellung des char-Typs +in Java verwenden sie 16-Bit breite UNICODE-Zeichen zur Ein- und Ausgabe. +Um eine Datei zu erzeugen, kann ein FileWriter-Objekt +angelegt werden, und die Zeichen werden mit den write-Methoden +geschrieben. Um die Performance zu erhöhen, kann der FileWriter +in einen BufferedWriter +gekapselt werden, der mit Hilfe eines internen Zeichenpuffers die +Anzahl der Schreibzugriffe reduziert. Im Test ergab sich dadurch ein +Geschwindigkeitszuwachs um den Faktor drei bis vier gegenüber +dem ungepufferten Zugriff. Die von BufferedWriter +verwendete Standard-Puffergröße von 8 kByte ist in aller +Regel ausreichend, weitere Vergrößerungen bringen keine +nennenswerten Beschleunigungen. + +
+Das Dilemma der Writer-Klassen +besteht darin, dass die meisten externen Dateien mit 8-Bit-Zeichen +arbeiten, statt mit 16-Bit-UNICODE-Zeichen. Ein FileWriter +führt also vor der Ausgabe eine Konvertierung der UNICODE-Zeichen +durch, um sie im korrekten Format abzuspeichern. Der Aufruf der dazu +verwendeten Methoden der Klasse CharToByteConverter +aus dem Paket sun.io +kostet natürlich Zeit und vermindert die Performance der Writer-Klasse. +Wesentlich schneller sind die (älteren) OutputStream-Klassen, +die nicht mit Zeichen, sondern mit Bytes arbeiten. Sie führen +keine aufwändige Konvertierung durch, sondern geben je Zeichen +einfach dessen niederwertige 8 Bit aus. Das spart viel Zeit und führte +im Test zu einer nochmals um den Faktor drei bis vier beschleunigten +Ausgabe (wenn auch der FileOutputStream +in einen BufferedOutputStream +eingeschlossen wurde). + +
+Die OutputStream-Klassen +sind also immer dann den Writer-Klassen +vorzuziehen, wenn entweder sowieso Binärdaten ausgegeben werden +sollen oder wenn sichergestellt ist, dass keine UNICODE-Zeichen verwendet +werden, die durch das simple Abschneiden der oberen 8 Bit falsch ausgegeben +würden. Da der UNICODE-Zeichensatz in den ersten 256 Zeichen +zum ISO-8859-1-Zeichensatz kompatibel ist, sollten sich für die +meisten europäischen und angelsächsischen Sprachen keine +Probleme ergeben, wenn zur Ausgabe von Zeichen die OutputStream-Klassen +verwendet werden. + +
+Listing 50.7 zeigt +das Erzeugen einer etwa 300 kByte großen Datei, bei der zunächst +die Writer- +und dann die OutputStream-Klassen +verwendet wurden. Im Test lag die Ausführungsgeschwindigkeit +der zweiten Variante um etwa eine Zehnerpotenz über der ersten. + + +
+
+
+
+001 public static void createfile1()
+002 throws IOException
+003 {
+004 Writer writer = new FileWriter(FILENAME);
+005 for (int i = 0; i < LINES; ++i) {
+006 for (int j = 0; j < 60; ++j) {
+007 writer.write('x');
+008 }
+009 writer.write(NL);
+010 }
+011 writer.close();
+012 }
+013
+014 public static void createfile4()
+015 throws IOException
+016 {
+017 OutputStream os = new BufferedOutputStream(
+018 new FileOutputStream(FILENAME)
+019 );
+020 for (int i = 0; i < LINES; ++i) {
+021 for (int j = 0; j < 60; ++j) {
+022 os.write('x');
+023 }
+024 os.write('\r');
+025 os.write('\n');
+026 }
+027 os.close();
+028 }
+
+ |
+
+Die Performance des sequenziellen Lesens von Zeichen- oder Byte-Streams +zeigt ein ähnliches Verhalten wie die des sequenziellen Schreibens. +Am langsamsten war der ungepufferte Zugriff mit der Klasse FileReader. +Die größten Geschwindigkeitsgewinne ergaben sich durch +das Kapseln des FileReader +in einen BufferedReader, +die Performance lag um etwa eine Zehnerpotenz höher als im ungepufferten +Fall. Der Umstieg auf das byte-orientierte Einlesen mit den Klassen +FileInputStream +und BufferedInputStream +brachte dagegen nur noch geringe Vorteile. Möglicherweise muss +der zur Eingabekonvertierung in den Reader-Klassen +verwendete ByteToCharConverter +weniger Aufwand treiben, als ausgabeseitig nötig war. + + + + +
+Der wahlfreie Zugriff auf eine Datei zum Lesen oder Schreiben erfolgt +in Java mit der Klasse RandomAccessFile. +Da sie nicht Bestandteil der Reader- +Writer-, +InputStream- +oder OutputStream-Hierarchien +ist, besteht auch nicht die Möglichkeit, sie zum Zweck der Pufferung +zu schachteln. Tatsächlich ist der ungepufferte byteweise Zugriff +auf ein RandomAccessFile +sehr langsam, er liegt etwa in der Größenordnung des ungepufferten +Zugriffs auf Character-Streams. Wesentlich schneller kann mit Hilfe +der read- +und write-Methoden +gearbeitet werden, wenn nicht nur ein einzelnes, sondern ein ganzes +Array von Bytes verarbeitet wird. Je nach Puffergröße und +Verarbeitungsaufwand werden dann Geschwindigkeiten wie bei gepufferten +Bytestreams oder höher erzielt. Das folgende Beispiel zeigt, +wie man mit einem 100 Byte großen Puffer eine Random-Access-Datei +bereits sehr schnell lesen kann. + + +
+
+
+
+001 public static void randomtest2()
+002 throws IOException
+003 {
+004 RandomAccessFile file = new RandomAccessFile(FILENAME, "rw");
+005 int cnt = 0;
+006 byte[] buf = new byte[100];
+007 while (true) {
+008 int num = file.read(buf);
+009 if (num <= 0) {
+010 break;
+011 }
+012 cnt += num;
+013 }
+014 System.out.println(cnt + " Bytes read");
+015 file.close();
+016 }
+
+ |
+
+Das Programm liest die komplette Datei in Stücken von jeweils +100 Byte ein. Der Rückgabewert von read +gibt die tatsächliche Zahl gelesener Bytes an. Sie entspricht +normalerweise der Puffergröße, liegt aber beim letzten +Datenpaket darunter, wenn die Dateigröße nicht zufällig +ein Vielfaches der Puffergröße ist. Die Performance von +randomtest2 ist sehr gut, sie +lag auf dem Testrechner (Pentium II, 266 MHz, 128 MB, UW-SCSI) bei +etwa 5 MByte pro Sekunde, was für ein Java-Programm sicherlich +ein respektabler Wert ist. Ein wesentlicher Grund ist darin zu suchen, +dass durch den programmeigenen Puffer ein Großteil der Methodenaufrufe +zum Lesen einzelner Bytes vermieden werden (in diesem Fall sind es +um den Faktor 100 weniger). Auf die gleiche Weise lassen sich auch +die streamorientierten Dateizugriffe beschleunigen, wenn die Anwendung +nicht unbedingt darauf angewiesen ist, zeichenweise zu lesen +bzw. zu schreiben. + + + + +
+Neben den direkten Prozessoraktivitäten hat auch die Art und +Weise, in der das Programm mit dem Hauptspeicher umgeht, einen erheblichen +Einfluss auf dessen Performance. Einige der Aspekte, die dabei eine +Rolle spielen, sind: +
| Titel + | Inhalt + | Suchen + | Index + | DOC + | Handbuch der Java-Programmierung, 5. Auflage, Addison +Wesley, Version 5.0.1 + |
| << + | < + | > + | >> + | API + | © 1998, 2007 Guido Krüger & Thomas +Stark, http://www.javabuch.de + |