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 machen

                 Binary 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 der StreamReader wohl für Textdateien gedacht ist - und auch der BinaryReader 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 eine FileNotFoundException, 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 Binary

    ByPu1.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.


  • Mod

    @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 macht StreamReader 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 und F12 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. direkt memcmp 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);
    }
    

    https://stackoverflow.com/a/48599119/454519



  • @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 interessant

    ByPu1.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 die using-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
    

Log in to reply