Re: Java vs. Pascal



Ivan Dolvich wrote:
Ralf Ullrich wrote:

Stringoperationen per se sollten unter Java nicht deutlich langsamer
sein, als mit anderen Sprachen [...]

Zustimm.

Das stimmt nicht immer, es hat was mit der Unveränderlichkeit von
Strings zu tun (alle String-Methoden liefern zwangsweise ein neues Objekt).

Neulich habe ich eine kleine Klasse reimplementiert, die Textdateien
parst. Das Format war sehr einfach, jede Zeile bestand aus einem Typ und
mehrere Werte, etwa so:

punkt 2.112 43.11 -312.23
punkt 4.55 455.5454 230.334
...
polygon 23/434/434
polygon 43/34/24

In der Originalversion wurde zeilenweise gelesen und dann mit
String.split() und Float.parseFloat() wurden die Werte geholt. Das
Problem dabei war dass die Textdateien sehr gross waren und der ganze
Parse-Vorgang sehr sehr viele temporäre Strings erzeugt hat. Für jede
Zeile und jeden Wert in der Zeile wurde ein temporärer String erzeugt.
Das waren insgesamt ca. 40 Milionen(!) kleine temporäre Objekte, die
auch den Garbage Collector belastet haben. Die Anwendung wurde dadurch
für 10 Sekunden sehr langsam. Wir haben dann den Parser sehr low-level
umgeschrieben, so dass er nur Zeichenweise liest und sich dabei die
float-Werte selbst aufbaut ohne temporäre Strings zu erzeugen - die
Laufzeit wurde 3 mal kürzer und die GC-Zeit 20 mal kürzer.

Mit Verlaub:
Das auf die 'vielen Objekte' zu schieben ist vereinfacht, gar falsch.
Es scheint mir eher ein vorschnelles Urteil wie 'Java ist langsam',
'Swing ist langsam', und so'n Zeug zu sein.

Kann natürlich sein das ich nun auch falsch liege, glaub' ich aber nicht:-)

Ich will hier gegen das Objekte-sparen antreten.


Kurzlebige Objekte sind nicht teuer. Den GC belasten die auch nicht,
die bleiben in der Nursery (nennt sich doch so, oder?) und werden
einfach nicht angefasst und sterben ohne Aufwand. Man muß nur sehen
das die nicht zu lange leben.
Und das z.B. parent.substring() eine Referenz auf parent behält, das
kann dann durchaus 10MB für ein "Hi" ausmachen.


Eigentlich müsste man genauer hinschauen (Profiler) ich hab' noch nicht
mal deinen Code.

Dein obiger Text klingt etwa so:
while(...){
String[] parts = string.split(" ");
bauWasDraus(parts);
}

Das bedeutet jedesmal ein neues Pattern compilieren. Das ist das erste
was man superbillig verbessern kann. (s.u.)

Ich nenn' mal die Zahlen von unten (jre1.5):
700 string.split() ~Regex
500 pattern.split() ~Regex
240 StringTokenizer
120 homegrown

Mit wiederverwendetem pattern hat man da schon 30% der Zeit gespart.
Ein Blick in Pattern#split() zur Inspiration könnte noch etwas
Transformiererei sparen.

Regex ist zwar nett und gar nicht so lahm, aber dann doch nicht das
flotteste wenn man einfachste splits hat, und die millioenmal aanwenden
will.
(Ansonsten nehm' ich die gerne, die Entwicklungszeit ist einfach
schnell.)

Die anderen Dinger unten (das split2 ist Jahre alt und lag halt hier rum)
sind dann nochmal bedeutend schneller. Objekte bauen die verschiedene
methoden auch gleich viele pro token.

Ich würde nun mal behaupten das du beim reimplementieren einfach die Menge
ausgeführter Anweisungen drastisch reduziert hast. Das rumfummeln mit
einzelnen char dürfte nicht der Bringer gewesen sein. Das selbst
implementieren von float parsern schon gar nicht. Höchstens verschwendete
Zeit und _insbesondere_ fehlerträchtig.


Das Potential der verschiedenen Methoden (RegEx, StringTokenizer, split2)
ist einfach deutlich abfallend, deshalb ist das am besten angepasste split2
am schnellsten.


Nicht die Creme de la Code, es geht nur um die Splitzeit:
-------------------------------------------------------------------
import java.util.*;
import java.util.regex.Pattern;

public class MiscSplit {

public static void main(String[] args) {
String s = "punkt 2.112 43.11 -312.23";
Pattern p = Pattern.compile(" ");
int N = 1000*100;

long t0 = System.nanoTime();
for(int i=0;i<N;i++){
// String[] parts = s.split(" "); // 700ms
// String[] parts = p.split(s); // 500ms
// split(s); // 240ms
split2(s, ' '); // 120ms
}
long t1 = System.nanoTime();

System.out.println("Zeit:" + ((t1-t0)/1000000.0) + " ms");
}

private static List<String> split(String s) {
StringTokenizer st = new StringTokenizer(s, " ");
List<String> res = new ArrayList<String>();
while(st.hasMoreTokens()){
res.add(st.nextToken());
}
return res;
}

public static List<String> split2(String s, char delimiter) {
if (s==null || s.length()==0) return new ArrayList<String>(0);

List<String> res = new ArrayList<String>();

int start = 0;
int next = s.indexOf(delimiter,start);

while (next!=-1){
if (start==next) res.add("");// empty token
else res.add(s.substring(start, next));
start = next+1;
next = s.indexOf(delimiter,start);
}
String last = s.substring(start);
if (last.length()>0) res.add(last);

return res;
}
}
--------------------------------------------------------------------


Ursache des ganzen ist aber die in manchen Punkten erbärmlich schlechte
Java Runtime, die jeden Scheiß abdeckt, tausende unnötige Konstruktoren
bereitstellt, aber so grundlegende Dinge mehr oder weniger ignoriert. So
ein Tools-package mit Dingen die man immer wieder braucht hätte schon in
1.1 reingehört (Files am Stück einlesen, split, join, cvs-lesen/schreiben,
Little/Big_endian-Leser/Schreiber, Streams kopieren, uvm.).




Grüße
Peter
.



Relevant Pages