Merkwürdige Laufzeitunterschiede C# / Delphi 7
-
Da ich mit dem aktuellen gratis-Delphi von Embarcadero Lizenz-Probleme hatte nach der erneuten Installation auf meinem neuen PC (der alte wurde durch den neuen ersetzt), muss ich wieder mein schon etwas betagtes Delphi 7 benutzen, das zwar immer noch bei Win10 und 11 funktioniert, aber doch nicht mehr der aktuelle Stand ist.
Eines meiner Programme hat die Aufgabe, den Inhalte zweier vorgegebener Ordner zu vergleichen. Das benutze ich nach Sicherungen, um sicher zu gehen, dass die gesicherten Dateien tatsächlich mit den Originalen identisch sind. Die Sicherungen können auf externe HDDs erfolgen, aber auch auf DVDs, Blurays , USB-Sticks usw.
Ich habe dieses Delphi-Programm in C# umgeschrieben, alles in etwas identisch.
Da Programm benutzt zwei möglichst große Puffer. Dateien, die nicht größer, als diese Puffer sind, werden komplett eingelesen, die größeren immer in Blöcken der Puffergröße und einen Restblock. Die Blöcke werden nach dem Einlesen verglichen.// 87654321 public const int MaxPufL = 0x01000000; // 16.777.216 Bytes //public byte[] ByPu1 = new byte[MaxPufL + 0x1000]; // für FileStream //public byte[] ByPu2 = new byte[MaxPufL + 0x1000]; public char[] ByPu1 = new char[MaxPufL + 0x1000]; // für StreamReader public char[] ByPu2 = new char[MaxPufL + 0x1000];
Ich habe jeweils einen Ordner auf einer superschnellen m.2 Samsung 980 mit sich selbst verglichen und bei dem C# Programm den Vergleich der Byte-Array übersprungen. Die Laufzeiten sind extrem unterschiedlich.
Der Order enthält ca. 15.000 Dateien, 770 Unterordner und die Dateien haben einen Umfang von ca, 18 GB.Das Delphi-Programm erledigt diesen Vergleich in 37 Sekunden.
Das C# Programm braucht etwa 28 Minuten = 1680 Sekunden => die 45-fache Zeit.
Wenn ich bei C# auch noch den Puffervergleich laufen lasse, dann dauert alles zusammen etwa 43 Minuten.Bei Delphi benutze ich für das Einlesen die BlockRead Funktion.
Bei C# habe ich StreamReader - ReadBlock aber auch FileStream - Read benutzt, da gibt es kaum einen Laufzeitunterschied. Die "alte" BlockRead-Funktion, die ich zu DOS-Zeiten bereits bei Turbo-Pascal benutzt habe, gibt es bei C# nicht mehr (oder ich habe sie nicht gefunden).Wie kann es sein, dass derartig extreme Laufzeitunterschiede vorhanden sind ?
-
Wie sieht denn dein C# Code dazu aus?
Und ist nur das Einlesen der Dateien langsam oder auch das Durchsuchen der Verzeichnisse?PS: Ist das immer noch der Code wie für Wird bei DllImport die DLL-Datei pro Aufruf geladen ? - nur daß du jetzt alles von C# aus erledigen möchtest, anstelle einer externen Lib?
-
@hkdd sagte in Merkwürdige Laufzeitunterschiede C# / Delphi 7:
Wie kann es sein, dass derartig extreme Laufzeitunterschiede vorhanden sind ?
Also wenn man die durchschnittliche Zeit für einen Dateivergleich betrachtet, so benötigt das C# Programm (1680-37)/15000s = 0.109s = 109ms länger pro Datei.
Ich würde da mal einen Blick in die Profiling Tools werfen, sofern du Visual Studio verwendest.
-
@Th69
hier ein paar Auszüge aus dem Programm.
Da ich den eigentlichen Vergleich stillgelegt habe, werden auch keine DllImport-Routinen ausgeführt.
Den Byte-Array Vergleich wollte ich mit Binary machenBinary b1 = new Binary(ByPu1); Binary b2 = new Binary(ByPu2); if(b1.Equals(b2)) { isDiff = 0; } // es gibt keine Differenzen else { isDiff = 1; } // es gibt Differenzen
Dabei ist mir nicht klar, ob die Zuweisung
Binary b1 = new Binary(ByPu1);
den ganzen Pufferinhalt von ByPu1 nach b1 umspeichert (nochmals unsinniger Aufwand)
oder ob man mit fs1.Read statt in ein byte[] Array auch in einen Binary-Puffer lesen kann.
Bei Binary kann man mit b1.Equals(b2) zwei Puffer direkt vergleichen, statt der urtümlichen
for-Schleife Byte für Byte oder externer [DllImport]-Routinen.
Bei Delphi mache ich den Vergleich mit einer Ass-Sequenz.using System; using System.IO; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; using System.Threading; using System.Globalization; namespace CompHK { public partial class Form1 : Form { // 87654321 public const int MaxPufL = 0x01000000; // 16.777.216 Bytes //public byte[] ByPu1 = new byte[MaxPufL + 0x1000]; // für FileStream //public byte[] ByPu2 = new byte[MaxPufL + 0x1000]; public char[] ByPu1 = new char[MaxPufL + 0x1000]; // für StreamReader public char[] ByPu2 = new char[MaxPufL + 0x1000]; //======================================================== // Datei 1+2 vergleichen //======================================================== int CompFile(FileInfo fi1, FileInfo fi2) { string Dsn1 = fi1.FullName; // Lw:\Pfad\name.ext string Dsn2 = fi2.FullName; // Lw:\Pfad\name.ext int rc = 0; // 0 = Vergleich OK / =1: Vergleich nicht OK int AnzDiff = 0; // Anzahl Differenzen dieser Datei long VerglLenGes = fi1.Length; // in der Länge der kürzeren Datei vergleichen if (fi2.Length < VerglLenGes) { VerglLenGes = fi2.Length; } if (!File.Exists(Dsn1)) // theoretischer Fehler { PutListV("FEHLER Datei1: " + Dsn1 + " fehlt"); return 1; } if (!File.Exists(Dsn2)) { PutListV("FEHLER Datei2: " + Dsn2 + " fehlt"); return 1; } StreamReader fs1 = new StreamReader(Dsn1); StreamReader fs2 = new StreamReader(Dsn2); //FileStream fs1 = new FileStream(Dsn1, FileMode.Open, FileAccess.Read); //FileStream fs2 = new FileStream(Dsn2, FileMode.Open, FileAccess.Read); long VerglRestLen = VerglLenGes; long VerglOffset = 0; int VerglLen = 0; int isDiff = 0; NxtPuVergleich: if (VerglRestLen > MaxPufL) { VerglLen = (int) MaxPufL; } else { VerglLen = (int) VerglRestLen; } int ln1 = fs1.ReadBlock(ByPu1, 0, VerglLen); int ln2 = fs2.ReadBlock(ByPu2, 0, VerglLen); //fs1.Read(ByPu1, 0, (int)VerglLen); //fs2.Read(ByPu2, 0, (int)VerglLen); isDiff = 0; // es gibt keine Differenzen TEST TEST TEST TEST (kein Vergleich) VerglRestLen = VerglRestLen - VerglLen; // neue Restlänge GesLen = GesLen + VerglLen; GesVglLen = GesVglLen + VerglLen; if (VerglRestLen > 0) { VerglOffset = VerglOffset + VerglLen; // Offset nächster Block für evtl. Diff-Anzeige goto NxtPuVergleich; } fs1.Close(); fs2.Close(); return 0; }
Das Lesen der Directorys ist eigentlich in C# viel einfacher, als bei Delphi.
Da hole ich jeweils die Dateinamen und die Namen der Unter-Directorys und die werden danach verarbeitet.//----------------------------------------- // Informationen aus Directory-1 holen //----------------------------------------- try { N1dirInfo = new DirectoryInfo(pN1Dir); // Directory (1) N1fiArr = N1dirInfo.GetFiles(); // Datei-Informationen AnzFiles1 = N1fiArr.Count(); // Anzahl Dateien N1DirArr = N1dirInfo.GetDirectories("*.*"); // Unter-Directory-Informationen AnzDir1 = N1DirArr.Count(); // Anzahl Unter-Directorys } catch { Abbruch("FEHLER beim Lesen von DirectoryInfo von " + pN1Dir); return; } //----------------------------------------- // Informationen aus Directory-2 holen //----------------------------------------- try { N2dirInfo = new DirectoryInfo(pN2Dir); // Directory (2) N2fiArr = N2dirInfo.GetFiles(); // Datei-Informationen AnzFiles2 = N2fiArr.Count(); // Anzahl Dateien N2DirArr = N2dirInfo.GetDirectories("*.*"); // Unter-Directory-Informationen AnzDir2 = N2DirArr.Count(); // Anzahl Unter-Directorys } catch { Abbruch("FEHLER beim Lesen von DirectoryInfo von " + pN2Dir); return; }
-
Mit scheint nur die Variante mit
FileStream.Read
sinnvoll zu sein (da derStreamReader
wohl für Textdateien gedacht ist - und auch derBinaryReader
ein Encoding als Parameter haben will).Kommt der massive Unterschied vom Lesen großer Dateien oder machst du vielleicht beim Loopen über alle Dateien etwas anders? So ein massiver Geschwindigkeitsunterschied deutet eher darauf hin, dass du irgendwas in C# nicht so machst, wie es gedacht ist (denke ich).
(Achtung: ich habe noch nie mit C# gearbeitet)
Edit: die beiden
if (!File.Exists(...))
können weg. Der Stream-Konstruktur wirft doch sowieso eineFileNotFoundException
, wenn es die Datei nicht gibt.Der Code danach mit den vielen Längen und dem goto sieht kompliziert aus. Warum nicht einfach aus beiden Dateien lesen, solange das möglich ist? Lohne sich ein Vergleich wirklich, wenn die Dateien unterschiedliche Länge haben? Muss der Puffer so groß sein? Größer ist nicht unbedingt besser, kann ja irgendwelche Cache-Effekte geben etc - auch ist nicht klar, dass der Buffer-Reuse eine gute Idee ist.
-
Hi,
was mir so auffällt:
C# und Java beneiden uns um "unsere" C++Destruktoren, deshalb haben sie sich etwas ausgedacht, damit Sie wenigstens etwas in der Richtung haben: nämlich using. Das solltest du nutzen (sowieso), da ich mir nicht sicher bin, ob ein close ausreicht.
Dann würde ich FileStreams nehmen und statt dem BinaryByPu1.Take(count1).SequenceEqual(ByPu2.Take(count2)
Ansonsten: messen, messen, messen. Wird es langsamer (also was nicht aufgeräumt), sind große Dateien das Problem oder viele Dateien, Blockgrösse ändern,...
-
@Jockelx
Using kann man nicht so einfach benutzen, wenn man zwei Dateien parallel einliest, wie bei einem Compare-Programm erforderlich.
SequenceEqual werde ich probieren.
Mit verschiedenen Puffergrößen habe ich probiert, kleinere Puffer verlängern die Laufzeit, das betrifft aber nur große Dateien.
DANKE für die Hinweise.@wob
Lohne sich ein Vergleich wirklich, wenn die Dateien unterschiedliche Länge haben
Das kann beim Programm eingestellt werden.
Oftmals macht das keinen Sinn, ich hatte aber auch schon Dateien, wo am Ende etwas angehängt wurde.
In diesem Fall wird in der kürzeren Länge verglichen.Das Laufzeitproblem hat aber mit dem eigentlich Vergleich nichts zu tun, weil ich ja nichts vergleiche (außer bei Delphi), sondern nur die Dateien einlese. Da gibt es auch keine Dateien mit unterschiedlicher Länge.
-
@hkdd Warum nicht?
using (var file1 = new FileStream(fileName1, FileMode.Open)) using (var file2 = new FileStream(fileName2, FileMode.Open)) { ... }
-
Ich habe ein kleines abgerüstetes Consolen-Programm geschrieben, das auch alle diese Dateien in ähnlicher Weise liest, wie mein anderes Programm. Da fehlt das Lesen der zweiten zu vergleichenden Datei, es gibt keinerlei Fehlerbehandlung usw.
Da dauert das Lesen nur wenige Sekunden.
Hier das Programm (es ist nicht groß)using System; using System.IO; using System.Linq; namespace AllFilesInDir // Alle Dateien eines Ordners (Kommandozeile) lesen { internal class Program { public static Int64 GesBytes = 0; // Summe aller gelesenen Bytes public static int AnzDir = 0; // Anzahl aller Directories incl. Start-Dir public static int AnzFiles = 0; // Anzahl aller gelesenen Dateien public static int AnzPuff = 0; // Anzahl aller gelesenen Puffer public const int MaxPuffL = 0x01000000; // 16.777.216 Bytes = Pufferlänge public static byte[] ByPu = new byte[MaxPuffL+0x1000]; // Eingabepuffer static void Main(string[] args) { DateTime dtStart= DateTime.Now; // Start-Zeitpunkt merken Console.WriteLine("Start = "+dtStart.ToString()); string StartPfad = args[0]; // Pfad aus Kommandozeile holen PfadVerarbeiten(StartPfad); Console.WriteLine(MaxPuffL.ToString("###,###,###,###,###") + " Bytes = Puffergröße"); Console.WriteLine(GesBytes.ToString("###,###,###,###,###") + " Bytes eingelesen"); Console.WriteLine(AnzDir.ToString("###,###,###,###,###") + " Directories"); Console.WriteLine(AnzFiles.ToString("###,###,###,###,###") + " Dateien"); Console.WriteLine(AnzPuff.ToString("###,###,###,###,###") + " Puffer"); DateTime dtEnde = DateTime.Now; // Ende-Zeitpunkt merken System.TimeSpan Dauer = dtEnde - dtStart; Console.WriteLine("Ende = " + dtEnde.ToString()+", Dauer = "+Dauer.ToString().Substring(0,10)); Console.ReadLine(); // Warten auf ENTER } static void PfadVerarbeiten(string pPfad) // wird rekursiv aufgerufen { AnzDir++; string sPfad = pPfad; if (sPfad[sPfad.Length - 1] != '\\') sPfad += "\\"; // ggf. Ende \ anfügen DirectoryInfo di = new DirectoryInfo(pPfad); // Aktuelles Directory FileInfo[] fi = di.GetFiles("*.*"); // Dateien im Directory int FilesAnz = fi.Count(); // Anzahl Dateien im Directory DirectoryInfo[] ui = di.GetDirectories("*.*"); // Unterordner im Directory int DirAnz = ui.Count(); // Anzahl Unterordner im Directory for (int i = 0; i < FilesAnz; i++) // Alle Dateien einlesen { LeseDatei(fi[i].FullName, fi[i].Length); } for (int i = 0; i < DirAnz; i++) // Alle Unterordner verarbeiten { PfadVerarbeiten(ui[i].FullName); } // Rekursiver Aufruf } static void LeseDatei(string pDsn, Int64 lenFile) { AnzFiles++; Int64 RestLen = lenFile; FileStream fs = new FileStream(pDsn, FileMode.Open, FileAccess.Read); Int64 LeseLen = Math.Min(RestLen, MaxPuffL); while(LeseLen > 0) { GesBytes += LeseLen; AnzPuff++; fs.Read(ByPu, 0, (int)LeseLen); RestLen -= LeseLen; LeseLen = Math.Min(RestLen, MaxPuffL); } } } }
Und hier das Ergebnis
Start = 05.12.2022 14:08:11 16.777.216 Bytes = Puffergröße 18.069.912.569 Bytes eingelesen 770 Directories 15.228 Dateien 16.152 Puffer Ende = 05.12.2022 14:08:13, Dauer = 00:00:02.5
Da muss ich doch nach einem Overhead suchen, der irgendwo versteckt liegt.
-
@hkdd sagte in Merkwürdige Laufzeitunterschiede C# / Delphi 7:
Da muss ich doch nach einem Overhead suchen, der irgendwo versteckt liegt.
Eine der ersten Antworten hier im Thread:
@Quiche-Lorraine sagte in Merkwürdige Laufzeitunterschiede C# / Delphi 7:
Ich würde da mal einen Blick in die Profiling Tools werfen, sofern du Visual Studio verwendest.
-
@hkdd
Wie @wob schon geschrieben hat machtStreamReader
ne Codepage-Konvertierung. Ich vermute mal da wird die Zeit draufgehen.@hkdd sagte in Merkwürdige Laufzeitunterschiede C# / Delphi 7:
Dabei ist mir nicht klar, ob die Zuweisung
Binary b1 = new Binary(ByPu1);
den ganzen Pufferinhalt von ByPu1 nach b1 umspeichert (nochmals unsinniger Aufwand)
Vermutlich ja. Kannst du aber ganz einfach rausfinden indem du den Cursor in der Zeile in das Wort
Binary
reinstellst undF12
drückst.oder ob man mit fs1.Read statt in ein byte[] Array auch in einen Binary-Puffer lesen kann.
Bei Binary kann man mit b1.Equals(b2) zwei Puffer direkt vergleichen, statt der urtümlichen
for-Schleife Byte für Byte oder externer [DllImport]-Routinen.
Bei Delphi mache ich den Vergleich mit einer Ass-Sequenz.Vergiss
Binary
. Das macht intern auch nix anderes als nen Loop in dem es einzeln die Bytes vergleicht. Assembler brauchst du auch nicht wirklich. Du kannst z.B. direktmemcmp
der MSVC CRT aufrufen:[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int memcmp(byte[] b1, byte[] b2, UIntPtr count);
-
ps: Wenn du .NET 6 verwenden kannst:
static bool ByteArrayCompare(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2) { return a1.SequenceEqual(a2); }
-
@Jockelx
die Klammern {...} beziehen sich doch auf das zweite using.
Eigentlich müsste es so aussehen:using (var file1 = new FileStream(fileName1, FileMode.Open)) { using (var file2 = new FileStream(fileName2, FileMode.Open)) { ... } }
Da fällt es nicht so leicht, das parallele Lesen von file1 und file2 zu synchronisieren.
@hustbaer,
msvcrt habe ich bereits in meinem Programm vorgesehen. Es ist halt auch eine externe [DllImport]-Routine.
SequenceEqual sieht schon besser (da intern) aus.@Jockelx
Deinen Vorschlag finde ich auch sehr interessantByPu1.Take(count1).SequenceEqual(ByPu2.Take(count2)
Ich finde es toll, wie man hier im Forum seine Probleme/Fragen beantwortet bekommt.
-
Die Klammern sind normale Block-Klammern, wie bei anderen Anweisungen auch (
if
,for
,while
), d.h. bei nur einer folgenden Anweisung können diese entfallen.
Ab C# 8 können sogar dieusing
-Anweisungen ohne Verschachtelung geschrieben werden (und gelten dann bis zum Ende des Blocks, in dem sie sich selbst befinden): using-Deklaration (es entfallen zur Syntaxerkennung dann die runden Klammern).using var file1 = new FileStream(fileName1, FileMode.Open); using var file2 = new FileStream(fileName2, FileMode.Open); // keine Blockklammern mehr nötig