Merkwürdige Laufzeitunterschiede C# / Delphi 7
-
@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