Thomas Kramer

IT-COW | Processing/Java: Vergleich von vier Varianten zum Datei-Laden

Processing/Java: Vergleich von vier Varianten zum Datei-Laden

By Administrator at Dezember 10, 2011 17:24
Filed Under: Java, Programmierung allgemein

In Bezug auf meine Projektarbeit zum Thema String-Matching hatte ich ein Phänomen in Java/Processing festgestellt - der java.exe-Prozess benötigte stets ein Vielfaches an RAM als die Testdateien groß waren.

 

In meinen Tests kristallisierte sich heraus dass die fehlende Initialisierung des StringBuilders mit der richtigen Länge wesentlich dafür verantwortlich ist, denn so muss der StringBuilder Umkopieraktionen in einen größeren Puffer durchführen - das ist nicht wesentlich Rechenzeit-, aber RAM-intensiv. Aber auch die Initalisierung des StringBuilders auf die genaue Dateigröße reicht nicht in jedem Fall aus, zumindest nicht wenn man zeilenweise ausliest.

 

Dazu an dieser Stelle zunächst einmal die Erwähnung dass ein Zeilenumbruch je nach Betriebssystem mit ein oder zwei Zeichen gespeichert wird – unter Windows normalerweise mit zwei Zeichen, siehe Wikipedia. Außerdem will ich auch erwähnen dass der readLine-Befehl vom BufferedReader in Java Ein- und Zwei-Zeichen-Zeilenumbrüche gleich behandelt, und genau das stellte das Problem dar.

 

Es ist in Java möglich die typischen System-Zeilenumbruchszeichen mit diesem Code auszulesen:

 

String sep = System.getProperty("line.separator");

 

Aber es hängt in erster Linie von der Datei selbst ab wie sie gespeichert ist, denn man kann ja auch Dateien mit Unix-Ursprung unter Windows öffnen. Wenn die Datei unter verschiedenen Betriebssystemen bearbeitet wurde kann es durchaus passieren dass sie sowohl Ein-Zeichen- als auch Zwei-Zeichen-Zeilenumbrüche enthält.

 

Wenn man eine Datei zeilenweise ausliest - welche Zwei-Zeichen-Zeilenumbrüche benutzt - und nach dem Hinzufügen dieser Zeile auch jeweils einen Zwei-Zeichen-Zeilenumbruch (\r\n) in den StringBuilder anhängt kann man u. U. auf eine letztendlich abweichende Stringlänge kommen - wenn sich eben doch vereinzelte Ein-Zeichen-Zeilenumbrüche in der Datei finden lassen. Das kann dazu führen dass sich der StringBuilder vergrößern muss, eine Umkopieraktion startet und signifikant mehr RAM benötigt.

 

Aber das zeilenweise Einlesen ist auch ungeeignet für String-Matching-Algorithmen, weil die Suchpositionen dann nicht mehr unbedingt korrekt sind. Man müsste bei der Vorgehensweise zuerst die System-Zeilenumbruchszeichen herausfinden und dann noch sicherstellen dass in den Suchdateien wirklich NUR diese verwendet werden. Da ist es sinnvoller direkt blockweise auszulesen - zu Testzwecken habe ich einmal ein kleines Programm für Processing (Java) mit vier Varianten zum Einlesen von Dateien geschrieben, siehe Quellcode weiter unten.

 

Nachfolgend meine Testergebnisse - den RAM-Verbrauch habe ich natürlich einzeln beobachtet, das heisst die jeweils anderen Varianten ausgeklammert und dann getestet.

 

Dateigröße auf Festplatte: 65784159

 

1.1) zeilenweises Einlesen mit nötigem Overhead:
Dateigröße im RAM: 65784171
Overhead: 12 Zeichen
1576 ms Zeit benötigt zum Datei-Laden.

RAM-Verbrauch: 160 MB.

 

1.2) zeilenweises Einlesen ohne die nötigen 12 Zeichen Overhead: 
Dateigröße im RAM: 65784171
Overhead: 12 Zeichen
2521 ms Zeit benötigt zum Datei-Laden.

RAM-Verbrauch: 420 MB.

 

2) zeichenweises Einlesen:
Dateigröße im RAM: 65784159
7533 ms Zeit benötigt zum Datei-Laden.

RAM-Verbrauch: 147 MB.

 

3) in 1000-Zeichen-Blöcken einlesen:
Dateigröße im RAM: 65784159
1175 ms Zeit benötigt zum Datei-Laden.

RAM-Verbrauch: 160 MB.

 

4) Mit der loadStrings-Methode in Array einlesen, und dann mit join und append in den Stringbuilder kopieren.
Dateigröße im RAM: 65784169

Overhead: 10 Zeichen
5011 ms Zeit benötigt zum Datei-Laden.

RAM-Verbrauch: 717 MB.

 

Ergänzung: In einem Forum hat man mir den folgenden Code empfohlen:

 

byte[] data = Files.readAllBytes(Paths.get("pfad"));

String s = new String(data);

 

Aber in den Docs von Oracle steht:

 

Commonly Used Methods for Small Files

Reading All Bytes or Lines from a File

If you have a small-ish file and you would like to read its entire contents in one pass, you can use the readAllBytes(Path) or readAllLines(Path, Charset) method. These methods take care of most of the work for you, such as opening and closing the stream, but are not intended for handling large files. The following code shows how to use the readAllBytes method:

Path file = ...;
byte[] fileArray;
fileArray = Files.readAllBytes(file); 

Wird also nicht für größere Dateien empfohlen.

 

Zusammenfassung: Die Methoden 2 und 3 erzeugen keinen Overhead und benötigen damit im RAM exakt soviel Zeichen wie auf Festplatte – die Methoden 1 und 4 erzeugen jeweils einen Überlauf von 12 bzw. 10 Zeichen.

 

Eine Schlussfolgerung ist somit dass beim gesplitteten Einlesen nach Zeilen keine exakte Stringlänge im RAM gemäß der Dateigröße garantiert werden kann. Bei der Variante 1 sind es wahrscheinlich deswegen 12 statt 10 Zeichen Overhead weil ich die Zeilenumbrüche nachträglich im RAM hinzugefügt hatte – nach der letzten Zeile muss aber nicht zwingend noch ein weiterer Zeilenumbruch erfolgen. Die weiteren 10 Zeichen Overhead lagen daran dass trotz überwiegenden Zwei-Zeichen-Zeilenumbrüchen sich fünf vereinzelte Ein-Zeichen-Zeilenumbrüche in dieser Datei befinden müssen.

 

Die Methode .toString() vom StringBuilder kostet übrigens definitiv Arbeitsspeicher in Java und machte bei mir den Unterschied zwischen 160 zu 290 MB RAM-Nutzung aus:

 

test=completeString.toString();

 

Demnach muss man in Java wohl direkt mit dem StringBuilder weiterarbeiten - das gilt aber nicht für das .NET-Framework, siehe auch meinen .NET-Vergleich.

 

Da Java intern 16-Bit-Unicode verwendet ist eine Datei mit 8-Bit-Codierung nach dem Einlesen im RAM mindestens doppelt so groß - auch wenn die Anzahl Zeichen identisch ist, denn dabei wird eben in größeren Einheiten gezählt. Ich habe daher nun vermieden den Overhead in Bytes zu zählen, denn es sind natürlich Anzahl Zeichen gemeint. Ein Zeichen benötigt in Java wegen Unicode-Codierung 2 Bytes.

 

Update 12.12.2011: Ich habe nun auch eine Version für Eclipse-Java programmiert, außerdem habe ich einen Vergleich mit dem .NET-Framework durchgeführt.

 

Update 17.01.2012: Den append-Befehl vom Stringbuilder gibt es überladen auch in dieser Variante: append(char[] str, int offset, int len). Dadurch kann man sich beim blockweisen Lesen den Rest nach der Schleife einsparen:

 

while ((buffer = br.read(ioBuf,0,1000)) != -1)
{
  /* eingelesene Zeile anhängen */
  result.append(ioBuf,0,buffer);
}

 

import java.lang.Math;
import java.util.concurrent.TimeUnit;
import java.io.File;

String test;

int stringBuilderOverhead = 0;
int fileLength = 0;
color c1 = color(0, 0, 0);
color c2 = color(255, 255, 255);   
color c3 = color(193, 185, 185);

// systemeigenes Zeilenumbruchszeichen ermitteln
String sep = System.getProperty("line.separator");       
String loadPath="C:/Users/thomas/Downloads/pg2600_2.txt";
StringBuilder completeString=new StringBuilder(0);

/*-----------------------------------------------------------------------------
*  Initialisierungen
*-----------------------------------------------------------------------------*/

void setup()
{   
  File file = new File(loadPath);
  fileLength = (int) file.length();  
  file = null
  println("Dateigröße auf Festplatte: " + fileLength); 
 
  println("\nzeilenweises Einlesen: ");
  loadFile1();
 
  println("\nzeichenweises Einlesen: ");
  loadFile2();
 
  println("\nin 1000-Zeichen-Blöcken einlesen: ");
  loadFile3();
//  test=completeString.toString();
 
  println("\nMit der loadStrings-Methode in Array einlesen, und dann mit join und append in den Stringbuilder kopieren.");
  loadFile4();
   
/* // Datei für Diff-Vergleich schreiben 
  try
  {
    FileWriter f1 = new FileWriter("C:/Users/thomas/Downloads/pg2600_2_2.txt");
    f1.write(completeString.toString());
    f1.close();
  } catch (IOException e) {
    println("Datei konnte nicht geschrieben werden!");
  } */
}

/*-----------------------------------------------------------------------------
*  Variante 1: zeilenweises Einlesen einer Textdatei mit StringBuilder-Overhead
*-----------------------------------------------------------------------------*/

void loadFile1()
{       
  int overhead = 0;
  /* nimm die Vorher-Zeit für Gesamtdurchlauf */   
  long completeTimeBefore = System.currentTimeMillis();       
   
  completeString = new StringBuilder(fileLength + stringBuilderOverhead);              
  try
  {
    String thisLine=null;
    BufferedReader br = new BufferedReader(new FileReader(loadPath));     
    while ((thisLine = br.readLine()) != null)
    {
      /* eingelesene Zeile anhängen */
      completeString.append(thisLine);
      /* Zeilenumbruch hinzufügen */
      completeString.append(sep);
    }
    br.close();
  } catch (IOException e) {
    println("Datei konnte nicht eingelesen werden!");
    return;
  }
   
  println("Dateigröße im RAM: " + (overhead=completeString.length()));   
  println("Overhead: " + (overhead-fileLength) + " Bytes");
 
  /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */   
  long completeTimeAfter = System.currentTimeMillis();     
  long completeTimeDiff   = completeTimeAfter - completeTimeBefore;       
  println(completeTimeDiff + " ms Zeit benötigt zum Datei-Laden.\n"); 
}

/*-----------------------------------------------------------------------------
*  Variante 2: zeichenweises Einlesen und zeichenweises Hinzufügen
*-----------------------------------------------------------------------------*/

void loadFile2()
{       
  /* nimm die Vorher-Zeit für Gesamtdurchlauf */   
  long completeTimeBefore = System.currentTimeMillis();       
 
  completeString = new StringBuilder(fileLength);            
  try
  {
    int thisChar;
    BufferedReader br = new BufferedReader(new FileReader(loadPath));        
    while ((thisChar = br.read()) != -1)
    {
      /* eingelesene Zeile anhängen */
      completeString.append((char) thisChar);
    }
    br.close();
  } catch (IOException e)
  {
    println("Datei konnte nicht eingelesen werden!");
    return;
  }            
   
  println("Dateigröße im RAM: " + completeString.length());   
 
  /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */   
  long completeTimeAfter = System.currentTimeMillis();     
  long completeTimeDiff   = completeTimeAfter - completeTimeBefore;       
  println(completeTimeDiff + " ms Zeit benötigt zum Datei-Laden.\n"); 
}

/*-----------------------------------------------------------------------------
*  Variante 3: in 1000-Zeichen-Blöcken einlesen und hinzufügen
*-----------------------------------------------------------------------------*/

void loadFile3()
{       
  /* nimm die Vorher-Zeit für Gesamtdurchlauf */   
  long completeTimeBefore = System.currentTimeMillis();       
   
  completeString = new StringBuilder(fileLength);            
  try
  {
    char[] ioBuf = new char[1000];     
    /* buffer liefer Anzahl gelesener Zeichen zurück, siehe auch
       http://www.dpunkt.de/java/Referenz/Das_Paket_java.io/4.html#read%28%29 */
 
    int buffer = 0;   
   
    BufferedReader br = new BufferedReader(new FileReader(loadPath));     
    while ((buffer = br.read(ioBuf,0,1000)) == 1000)      
    {
      /* eingelesene Zeile anhängen */
      completeString.append(ioBuf);
      /* Array wieder leeren falls nur weniger Zeichen eingelesen werden können */
      ioBuf = new char[1000];             
    }
    /* die letzten gelesenen Zeichen müssen auch hinzugefügt werden */
    /* -1 wäre wenn von vornherein gar kein Zeichen eingelesen werden konnte */
    if (buffer != -1)
    {
      for (int i=0;i<buffer;i++)
        completeString.append(ioBuf[i]);
    }
    br.close();
  } catch (IOException e)
  {
    println("Datei konnte nicht eingelesen werden!");
    return;
  }            
   
  println("Dateigröße im RAM: " + completeString.length());   
 
  /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */   
  long completeTimeAfter = System.currentTimeMillis();     
  long completeTimeDiff   = completeTimeAfter - completeTimeBefore;       
  println(completeTimeDiff + " ms Zeit benötigt zum Datei-Laden.\n"); 
}

/*-----------------------------------------------------------------------------
*  Variante 4: Laden mittels loadStrings-Methode
*-----------------------------------------------------------------------------*/

void loadFile4()

  int overhead=0;
  /* nimm die Vorher-Zeit für Gesamtdurchlauf */   
  long completeTimeBefore = System.currentTimeMillis();       

  /* eigentliches Einlesen */
  String lines[] = loadStrings(loadPath);
  completeString.setLength(0);
  completeString.append(trim(join(lines, sep)));
  lines = null;   

  println("Dateigröße im RAM: " + (overhead=completeString.length()));   
  println("Overhead: " + (overhead-fileLength) + " Bytes"); 
 
  /* nimm die Danach-Zeit für Gesamtdurchlauf und bestimme Differenz */   
  long completeTimeAfter = System.currentTimeMillis();     
  long completeTimeDiff   = completeTimeAfter - completeTimeBefore;       
  println(completeTimeDiff + " ms Zeit benötigt zum Datei-Laden.\n");     
}

/*-----------------------------------------------------------------------------
*  draw()-Funktion wird aufgerufen wenn der Bildschirm neu gezeichnet wird
*-----------------------------------------------------------------------------*/

void draw()
{
  /*-----------------------------------------------------------------------------
   *  Hintergrundfarbe setzen, dabei wird auch der gesamte Bildschirm gelöscht
   *-----------------------------------------------------------------------------*/

  background(c3);   
 
  /*-----------------------------------------------------------------------------
   *  Überschriften setzen
   *-----------------------------------------------------------------------------*/

  fill(c2);
  textSize(20);
}

 

Pingbacks and trackbacks (1)+

Kommentar schreiben




  Country flag
biuquote
  • Kommentar
  • Live Vorschau
Loading


Tag-Wolke

Monats-Liste