![]() |
Viele Dateien performant einlesen
Ich habe eine Vielzahl (ein paar Tausend) CSV-Dateien. Die möchte ich allesamt auswerten.
Der naive Ansatz
funktioniert auch bestens. Es dauert aber zu lange. Bei ca. 5000 Dateien zu 24 KB braucht das Delphi-Programm auf meinem System (mit SSD) ca. 45 Sekunden. Der Flaschenhals ist eindeutig (und überraschenderweise)
Delphi-Quellcode:
. Lade ich genau die gleiche Datei ein zweites mal ein, ist das in "Null Millisekunden" getan.
TStringList::LoadFromFile
Es wird also technisch:
Was muss ich tun? Gibt es etwas, um alle Dateien einmal am Stück zu holen? Windows' Memory Mapped Files habe ich bislang immer nur für das Gegenteil- Viele, andauernde Operationen auf großen Dateien gehört... |
AW: Viele Dateien performant einlesen
Beim zweiten Mal ist die Datei noch im Cache, dann geht das schneller.
Evtl. der Virenscanner? |
AW: Viele Dateien performant einlesen
An Virenscanner habe ich jetzt nicht gedacht. Aber ja, das sollte ich auch mal testen.
Aber dass es am Windows-Datei-Cache liegt ist klar. Ich weiß von Anfang an, welche Dateien ich mir jetzt anschauen werde- Deshalb muss ich die doch sicher alle auf einmal einlesen können? |
AW: Viele Dateien performant einlesen
Zitat:
|
AW: Viele Dateien performant einlesen
Ich weiß nicht. Deshalb frage ich ja.
Meine Hoffnung ist, dass es deutlich besser wird zu sagen Zitat:
Zitat:
|
AW: Viele Dateien performant einlesen
Nein, kannst du nicht.
Du kannst maximal das machen, was z.B. MSOffice, Adobe und Co. machen. Die haben einen Service im Hintergrund laufen, welcher ihre (vermutlich) benötigten Dateien bereits beim Windowsstart und regelmäßig immer wieder läd, damit sie in der Windows-File-Cache landen und es dann schneller geht, wenn du irgendwann eventuell mal dein Office startest. Du könntest die Dateiliste schneller erstellen, denn auch das auslesen (vorallem großer Verzeichnisse) dauert "ewig". Aber dafür müsstest du dich z.B. direkt an die MFT wenden. (aber auch so kann man ein PreFetch erzwingen, indem das Verzeichnis schonmal vorher in den WFC geladen wird) OK, wenn du Admin Backup-Rechte besitzt, dann kannst du die Dateien direkt aus dem Dateisystem ziehen und wenn du diese vorher schön hintereinander gelegt hast (defragmentiert und neu positioniert), dann könntest du sie nun in einem Ruck schnell einlesen. :stupid: |
AW: Viele Dateien performant einlesen
Du könntest aber mehrere Threads zur Verarbeitung einsetzen, vielleicht wird es dann schneller.
|
AW: Viele Dateien performant einlesen
Zitat:
|
AW: Viele Dateien performant einlesen
Bei einer SSD kommt es auf die Controller an, wie viele Leseoperationen dort parallel behandelt werden können. Und auch auf die anderen Caches, wie und ob sie beim MultiThreading die Daten verwalten/blocken.
Bei Festplatten macht ein paralleles Lesen alles viel Schlimmer, da der Lesekopf dort womöglich/bestimmt mehr hin und her rennen muß, als wenn man das sequentiell lesen würde ... Besser wird nichts, höchstens langsamer. (außer die Daten liegen bereits im Cache) Das Einzige was man noch machen kann, ist die Cache zu umgehen. ( ![]() Beim "ersten" Laden ist es dann einen Hauch schneller, wenn andere Daten nicht erst aus dem Cache rausgeworfen werden müssen. Aber beim zweiten Durchgang ist es dann langsamer, da die Dateien ja nicht im Cache gelandet sind. Allerdings ist es insgesamt dennoch schneller, wenn man mehr Daten lesen will, als freier RAM zur Verfügung steht. Deine Dateien sind aber auch so klein, daß vermutlich ![]() MMF ist eher für RandomAccess geeignet, oder wenn man direkt in dem speicher mit Pointern rumrennen will, ohne einen eigenen Buffer verwalten zu müssen. Und bei so kleinen Dateien bringt das keinen großen Vorteil. Wobei auch und vorallem hier die Datei erst (nahezu unkontroliert) vom windows erst in den WFC geladen werden muß, dessen Speicher dann windows in deinen virtuellen RAM mappt/verlinkt. |
AW: Viele Dateien performant einlesen
Hatte hier im Forum mal was von einem "BULK INSERT" gelesen, habe allerdings keine Ahnung (da noch nie gemacht) ob dieser auch für viele Dateien geeignet ist.
|
AW: Viele Dateien performant einlesen
Bulkinsert ist eine Verfahren für den schneller Import in einen (MS)SQL-Server.
|
AW: Viele Dateien performant einlesen
Zitat:
Das Einzige was ihn noch einen Hauch schneller machen könnte, ist dann die Behandlung der eingelesenen Daten. Seit Delphi 2009 arbeiten StringStream, StringList und co. noch mit einem zusätzlichem Zwischenschritt, wo das Encoding auf Unicode umgewandelt wird. |
AW: Viele Dateien performant einlesen
Zitat:
|
AW: Viele Dateien performant einlesen
Ich muss zugeben, keine sonderlich ausführlichen Tests gefahren zu haben. Zwei mal
Delphi-Quellcode:
auf die exakt gleiche Datei hintereinander. Das zweite mal geht praktisch sofort. Das meinte ich.
myStringList.ReadFromFile(..)
In den Innereien von
Delphi-Quellcode:
habe ich jetzt nicht gewühlt...
TFileStream
|
AW: Viele Dateien performant einlesen
Ich schlage vor du änderst das Konzept.
Fass die z.B. 5000 Dateien zu einer zusammen, die im ersten Schritt importiert wird. Sollte sich eine der Dateien nachträglich ändern, wird die geänderte Version zusätzlich in das Verzeichnis gespeichert. Im zweiten Schritt werden diese Änderungen importiert. Nützlich ein optionaler dritter Schritt, der die Datei mit allen Änderungen wieder erstellt und die dann überflüssigen Einzeldateien löscht. Alternativ zum Löschen kann man auch mit den Dateiattributen(Archiv) oder Änderungszeitpunkt arbeiten. |
AW: Viele Dateien performant einlesen
Zitat:
Sollte sich eine der Dateien nachträglich ändern, wird die geänderte Version zusätzlich in das Verzeichnis gespeichert. Wenn sich die Dateien immer mal wieder ändern oder neue hinzukommen, würde ich ein ShellNotify auf das Verzeichnis setzen, dann hat man den zeitlichen Aufwand nur beim ersten Mal. Mit dem ShellNotify muss man aber das Konzept nicht mehr ändern. |
AW: Viele Dateien performant einlesen
Schau mal, was passiert, wenn du direkt mit
![]() Da du ja anscheinend weißt, dass deine Dateien <50KB sind, würde ich einfach mal 50KB Puffer reservieren und die Daten (damit möglichst "auf einmal") lesen. Bei einer SSD auch ruhig mal mit 2 Threads parallel testen ob es schneller ist. |
AW: Viele Dateien performant einlesen
Zitat:
Wobei auch TStringList alles auf einmal einliest. (stückchenweise ist denen von Emba und eigentlich fast Allen zu aufwändig :roll:) |
AW: Viele Dateien performant einlesen
Ich hatte mal ein halbwegs vergleichbares Problem. Die Aufgabe war, Text-Dateien der Größe 2-4 GB nach Mustern zu durchsuchen. In einem Moment des Denkverzichts hatte ich versucht eine solche Datei mit Notepad zu öffnen. :oops:
Anschließend habe ich stattdessen Edit++ verwendet. Der konnte "ganz locker" damit umgehen. Mit der FastStr-Bibliothek hats dann auch in meinem Programm sehr gut geklappt. Wenn ich mich recht erinnere lagen die Zeiten für das Lesen & Durchsuchen unter 10 Sekunden. Langer Rede, kurzer Sinn. Ich würde mal versuchen mit Edit++ oder sonst einem vergleichbaren Editor alle Dateien auf einen Rutsch zu öffnen und die Ladezeit (abzüglich einem eventuellen Overhead für den Aufbau der GUI) mit Deinen Zahlen zu vergleichen. Sollte das Ergebnis besser ausfallen, machen diese Editoren womöglich irgendwas anders als ein "TStringList.LoadFromFile" und ich würde das bereits vorgeschlagene Zusammenfassen der kleinen Dateien zu einer großen ernsthaft in Erwägung ziehen. Auch wenn dann weitere oder andere Herausforderungen auf Dich zukommen. Möglich, dass ich hier Äpfel mit Birnen vergleiche, aber vielleicht hilft es ja doch irgendwie. :roll: |
AW: Viele Dateien performant einlesen
ca. 120 MB? LOL ist doch heute nix mehr...
Blockread direkt ins Ram aller Dateien und dann verarbeiten? Mavarik |
AW: Viele Dateien performant einlesen
Zitat:
Imho kann man höchstens die Tatsache ausnutzen, das es sich um eine SSD handelt und die Dateien wirklich über mehrere Threads einlesen. Die Directory ist ja schnell gelesen und so ein Thread zum einlesen ist auch Einsfixdrei gebaut. Dann kann man doch einfach prüfen, ob es was bringt... |
AW: Viele Dateien performant einlesen
Zitat:
Wir hatten den Fall, der Katalog eines Herstellers besteht aus mehr als 1000 kleinen Dateien, die die täglichen Änderungen der Stammdaten enthalten und ständig erweitert werden. Unser Testsystem importiert alle Änderungen und wir erstellen ca. einmal im Jahr oder bei Bedarf eine große Datei. Wenn unsere Kunden diesen Katalog benötigen, wird bei der Installation sehr viel Zeit gespart. Neben der großen Datei müssen nur die nachfolgenden Änderungen importiert werden. |
AW: Viele Dateien performant einlesen
Bei diesem Ansatz kann man durch einen Thread etwas mehr herausholen.
Einer Klasse wird der Dateiname übergeben. Diese Klasse hat eine Eigenschaft, die diese Datei als TStringList (oder TStrings) zur Verfügung stellt. Allerdings wird beim Zugriff auf diese Eigenschaft erst der Inhalt der Datei geladen (wenn dies in der Zwischenzeit noch nicht erfolgt ist). Wenn sich nun jede Instanz an einen Thread hängt, der das Laden der Dateien in den Instanzen bewirkt, dann kann die Verarbeitung und das Laden der Datei in unterschiedlichen Threads erfolgen und die Zeit reduziert sich im optimalen Fall auf die reine Ladezeit / Verarbeitungszeit der Dateien (je nachdem, was länger dauert). Wenn das reine Laden 30 Sekunden dauert und die Verarbeitung 15 Sekunden, dann kann das gesamte Laufzeitverhalten bis auf 30 Sekunden reduziert werden. Das geht natürlich nur, wenn die Verarbeitung keine Festplatten-Aktionen erfordert, denn dadurch wird ja die Ressource Festplatte wieder eingebunden. UPDATE Wenn die reine Verarbeitung nur einen Bruchteil der gesamten Abarbeitung beansprucht, wird dieses Konstrukt aber uninteressant und schafft nur eine weitere Komplikation :) |
AW: Viele Dateien performant einlesen
Zitat:
|
AW: Viele Dateien performant einlesen
Zitat:
Gruß K-H |
AW: Viele Dateien performant einlesen
Letztendlich sind das alles Ratespiele. Warum nicht zielgerichtet vorgehen und einen Profiler einsetzen. Dann die Teile mit dem größten Einfluss auf die Performance optimieren, indem man z.b. Das Encoding selber setzt anstatt das die Stringliste selber machen zu lassen usw.
|
AW: Viele Dateien performant einlesen
Zitat:
Mit der OmniThreadLibraray könnte man sich auch eine Pipeline bauen, wo Einlesen und Verarbeiten separate Schritte sind. Dann kann man darauf weiter aufbauen und jeden Schritt nochmals in sich optimieren. |
AW: Viele Dateien performant einlesen
Ja, aber er hat LoadFromFile nicht profiled. Wenn man das echt instrumentieren will, muß man notfalls eine Kopie davon machen, an der man profiled und optimiert. Zum Schluß hat man dann automatisch eine "Ladeoptimierte" TStringList.
|
AW: Viele Dateien performant einlesen
Da es aber schnell genug läuft, wenn die Dateien schon im Cache liegen, dann ist das Encoding der TStringList vernachlässigbar.
Ansonsten hatte ich ja eigentlich schon alles erklärt. |
AW: Viele Dateien performant einlesen
Fragt sich was das für ein Cache ist? Wenn ich folgendes Fragment vorschalte, in dem ich die Daten komplett einlese, wird das Verarbeiten danach wesentlich schneller:
Delphi-Quellcode:
Diese Funktion braucht für 5.000 frisch erzeugte Dateien zwischen 500-1500 ms.
for i := Low(AFiles) to High(AFiles) do
begin Watch.Start; fs := TFileStream.Create(AFiles[i], fmExclusive); SetLength(Buffer, fs.Size); fs.ReadBuffer(Buffer[0], fs.Size); Watch.Stop; fs.Free; end; Wenn ich dann die Daten nochmals in Stringlisten einlese, werden 605-615 ms verbraucht. Starte ich das Einlesen in die Stringlisten ohne den Vorlauf, braucht das beim ersten Mal 12000-13000 ms.
D.h. dieses Verfahren ist 6x schneller! Komplettes Testprogramm:
Delphi-Quellcode:
unit main;
interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Diagnostics; type TForm1 = class(TForm) btnCreateData: TButton; btnRead: TButton; MemoProt: TMemo; btnOpen: TButton; procedure btnCreateDataClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure btnReadClick(Sender: TObject); procedure btnOpenClick(Sender: TObject); private { Private-Deklarationen } FDir : string; FWatch : TStopWatch; procedure Prot(const AMessage : string; AWatch : TStopWatch); public { Public-Deklarationen } property Watch : TStopWatch read FWatch; end; var Form1: TForm1; implementation uses System.Types, System.IOUtils, System.StrUtils; {$R *.dfm} procedure TForm1.btnCreateDataClick(Sender: TObject); var i : integer; Line : string; begin Watch.Start; Line := Dupestring('ABCDEFGHI;', 2400); for i := 1 to 5000 do TFile.WriteAllText(TPath.Combine(FDir, Format('%4.4d.txt', [i])), Line); Watch.Stop; Prot('CreateFiles', Watch); Watch.Reset; end; procedure TForm1.btnOpenClick(Sender: TObject); var AFiles : TStringDynArray; fs : TFileStream; i : integer; Buffer : TByteDynArray; begin Watch.Start; AFiles := TDirectory.GetFiles(FDir); Watch.Stop; Prot('GetFiles', Watch); Watch.Reset; for i := Low(AFiles) to High(AFiles) do begin Watch.Start; fs := TFileStream.Create(AFiles[i], fmExclusive); SetLength(Buffer, fs.Size); fs.ReadBuffer(Buffer[0], fs.Size); Watch.Stop; fs.Free; end; Prot('Open', Watch); Watch.Reset; end; procedure TForm1.btnReadClick(Sender: TObject); var AFiles : TStringDynArray; i : integer; Line : string; sl : TStringList; SplitWatch : TStopWatch; begin Watch.Start; AFiles := TDirectory.GetFiles(FDir); Watch.Stop; Prot('GetFiles', Watch); Watch.Reset; sl := TStringList.Create; sl.Delimiter := ';'; try SplitWatch := TStopWatch.Create; for i := Low(AFiles) to High(AFiles) do begin Watch.Start; Line := TFile.ReadAllText(AFiles[i]); Watch.Stop; SplitWatch.Start; sl.DelimitedText := Line; SplitWatch.Stop; end; Prot('ReadFiles', Watch); Prot('Split', SplitWatch); finally SplitWatch.Reset; Watch.Reset; sl.Free; end; end; procedure TForm1.FormCreate(Sender: TObject); begin FDir := TPath.Combine(TPath.GetDirectoryName(Application.ExeName), 'Data'); TDirectory.CreateDirectory(FDir); FWatch := TStopWatch.Create; end; procedure TForm1.Prot(const AMessage : string; AWatch : TStopWatch); begin MemoProt.Lines.Add(AMessage + ' ' +AWatch.ElapsedTicks.ToString+' ticks, '+AWatch.ElapsedMilliseconds.ToString+' ms'); end; |
AW: Viele Dateien performant einlesen
Liste der Anhänge anzeigen (Anzahl: 1)
..."Ich habe eine Vielzahl (ein paar Tausend) CSV-Dateien. Die möchte ich allesamt auswerten."...
Nun ja, ich hätte da ein paar Millionen(~3Mio) Dateien(a ~8KB) in ein paar tausend Verzeichnissen(~180000) und renne da ständig durch... wo ist das Problem: - über alle Dateien * FileStream öffnen und StreamSize ermitteln * bei den "Spielzeuggrößen" mit einem Schlag vom FileStream in RAM Buffer einlesen * FileStream freigeben(schließen) * keine Verarbeitung oder sonst was! * nächste Datei - Zeit messen Man könnte auch hier zwar per WinApi Funktionen und direktem RAM Puffer ein paar MicroSekunden sparen, aber FileStream reicht hier vollkommen aus. (Nebenbei: Bei "großen Dateien" einfach mal fach TFastfileStream googeln... da findet sich dann was schönes mit MemoryMappedFiles:)) Wenn das OK, dann mit etwas Hirnschmalz ein eigenes "GetLine" geschrieben(einfach per BYTE Pointer durch den Rambuffer nach "CR" suchen und wenn CR gefunden es durch "NULL" ersetzen und die nun "NullTerminierte" Line ab vorherigem Pointer(bzw. anfangs "@0") weiter auswerten. Gleiches Spiel nun innerhalb der Line. CSV-Trennzeichen fortlaufend suchen und gefundene Positionen+1 in BytePtrArray speichern und Inhalt des Pointers auf "0" setzen... Bingo: nun haben wir ohne jegliches Umkopieren irgendwelcher Speicher/Strings ein PtrArray mit lauter Pointern vom Typ PChar(bzw. PAnsiChar), welche wir problemlos verarbeiten, vergleichen, kopieren oder was auch immer können... (Sorry für meine Vorliebe für NullTerminierte Strings auf Pointer-Basis, ich denke immer noch in "C" und nutze Delphi/Pascal nur weil es sein muss;)) TStringList ist zwar schön, aber im Masseneinsatz ist deren LoadFromXXX als "ReadLN" doof. Wenn es schnell gehen soll, dann 3 Threads... 2x FileStreamLoad, 1x "RAM-CSV-Explode"... Results dann in weiterem Thread speichern/verarbeiten... damit sollte QuadCore gut ausgelastet werden. |
AW: Viele Dateien performant einlesen
Zitat:
Die von dir gemessenen Ergebnisse kann ich aber überhaupt nicht nachvollziehen:
Code:
CreateDataClick
CreateFiles 0:00:14.527 ReadClick GetFiles 0:00:00.007 ReadFiles 0:00:01.692 Split 0:00:03.112 CreateDataClick CreateFiles 0:00:09.681 OpenClick GetFiles 0:00:00.006 Open 0:00:02.614 ReadClick GetFiles 0:00:00.006 ReadFiles 0:00:01.649 Split 0:00:03.336 |
AW: Viele Dateien performant einlesen
Zitat:
Nach Deiner Methode mußt Du jede erstellte Zeile überprüfen ob sie vollständig ist, da doch lieber gleich richtig. Gruß K-H |
AW: Viele Dateien performant einlesen
Doch, also bei mir ist es ähnlich:
Code:
openClick
GetFiles 50269 ticks, 16 ms Open 745099 ticks, 238 ms readClick GetFiles 90555 ticks, 29 ms ReadFiles 3411398 ticks, 1094 ms Split 15388554 ticks, 4935 ms stringListRead 3179206 ticks, 1019 ms
Delphi-Quellcode:
ist jetzt einfach nur ein TStringList-LoadFromFile, also im Endeffekt praktisch das gleiche wie ReadFiles nur mit einem leicht anderen
stringListRead
Delphi-Quellcode:
zum Öffnen der Dateien.
FileMode
Jetzt das Wichtige: Wir übersehen bei diesen Benchmarks hier alle den Windows-Cache hinter den Kulissen! Ich war grade auch verdutzt über meine Zeiten: Wie kam ich denn auf 45 Sekunden? Dann habe ich ![]() Um aussagekräftige Messungen zu machen sollte man wohl vor jedem Durchgang einmal komplett den Windows File Cache leeren. Das werde ich im Verlauf des Tages auch nochmal machen... Auf jeden Fall vielen Dank für die rege Teilnahme an alle! :thumb: |
AW: Viele Dateien performant einlesen
Zitat:
![]() |
AW: Viele Dateien performant einlesen
Die Antwort ist 134217728 ;)
Delphi-Quellcode:
Damit liest er die 5000 Dateien in 160-400 ms komplett ein, auch wenn diese gerade frisch erzeugt wurden.
procedure TForm1.btnOpenLowLevelClick(Sender: TObject);
var AFiles : TStringDynArray; fh : THandle; fl, fr : LongWord; i : integer; Buffer : Pointer; begin Watch.Start; AFiles := TDirectory.GetFiles(FDir); Watch.Stop; Prot('GetFiles', Watch); Watch.Reset; for i := Low(AFiles) to High(AFiles) do begin Watch.Start; fh := CreateFile(PChar(AFiles[i]), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_SEQUENTIAL_SCAN, 0); // Optimiert für nur vorwärts lesen fl := GetFileSize(fh, nil); fr := 0; GetMem(Buffer, fl); ZeroMemory(Buffer, fl); ReadFile(fh, Buffer^, fl, fr, nil); CloseHandle(fh); FreeMem(Buffer); Watch.Stop; end; Prot('Lowlevel open', Watch); Watch.Reset; end; |
AW: Viele Dateien performant einlesen
Zitat:
Zitat:
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 00:22 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz