Wird bei DllImport die DLL-Datei pro Aufruf geladen ?



  • @Th69
    mit einmaligem Aufruf - das ist logisch, geht aber nicht, wenn man nur bestimmte Aufgaben, wie einen Vergleich von zwei Puffern auslagern will und nicht das ganze Programm. Ich habe das gleiche Programm in Delphi - und das funktioniert sehr schnell. In C# kann ich die Ass-Routine nicht benutzen, deshalb die Auslagerung in eine C++ DLL.
    Kann man denn die vom cl-Cpompiler (VS 2022 c++) erstellten Dateien LIb, OBJ usw. nicht benutzen und damit auf DllImport verzichten ?
    Oder kann man bei C# keine OBJ Dateien von anderen Compilern benutzen ?
    Man benutzt ja auch System-Routinen, die bestimmt nicht alle in C# programmiert sind.
    Diese Dateien erstellt der cl-Compiler/Linker

    22.09.2022  10:09                25 MakeZaehleDLL.bat
    22.09.2022  10:12               114 Zaehle.cpp
    22.09.2022  10:12            80.896 Zaehle.dll
    22.09.2022  10:12               632 Zaehle.exp
    22.09.2022  10:12             1.678 Zaehle.lib
    22.09.2022  10:12               657 Zaehle.obj
                   6 Datei(en),         84.002 Bytes
    


  • Ich bin jetzt nicht sonderlich fit in C#, aber ich gehe davon aus, dass die C# Beispielschleife einfach schon zur Compiletime ausgewertet wird, es stehen ja alle Informationen zur Verfügung. Um das etwas realistischer zu gestalten könnte man das m als Kommandozeilenparameter übergeben.

    Was ich online noch gefunden habe, um dll Aufrufe etwas performanter zu machen: [SuppressUnmanagedCodeSecurity] (https://learn.microsoft.com/en-us/visualstudio/code-quality/ca2118?view=vs-2022)



  • BTW: Du kannst Assembler in C# schon auch nutzen wenn es sein muss: https://www.codeproject.com/Tips/855149/Csharp-Fast-Memory-Copy-Method-with-x-Assembly-Usa

    Mich wundert ein bisserl, dass das Byte-Array-Comparen wirklich so viel langsamer sein soll in .NET 6 als mit Assembler (das sind ja Größenordnungen von denen du da sprichst...), also vllt. ist da auch nur eine Performance-Krücke in deinem C#-Code.

    MfG SideWinder



  • SideWinder,
    ich vergleiche etwa 18 TB Daten, es sind 768 Unter-Ordner und 15.214 Dateien.
    Den maximale Puffer zum Einlesen habe ich ab 1 GB festgelegt.
    18 TB sind etwa 18.000 GB. Kleinere Dateien ( < 1 TB ) werden komplett eingelesen und verglichen.
    Die DLL-Routine mit dem Compare-UP (Vergleich von den Pufferinhalten, max. 1 TB) wir also etwa 18.000 mal aufgerufen. Da wird bei DllImport bei jedem Aufruf der Routine die DLL-Routine geladen usw. Deshalb wahrscheinlich diese lange Laufzeit von knapp einer halben Stunde. Mein Delphi-Programm vergleicht diese Datenmenge in weniger als 40 Sekunden.

    CompHKw - Dateien Vergleichen - (C) 07.09.2022 H.Kresse, Dresden <= Das ist das Delphi-Programm
    ---------------------------------------------------------
    Parameter: "U:\#hk" "U:\#hk" "/U" "/VJ" "/F" "/M:2" "/A"
    ---------------------------------------------------------
    Vergleich: U:\#hk\
          mit: U:\#hk\
    ~~~~~~~~~~~~~~~~~~~
    15.214.Datei: U:\#hk\ZX81\Tapes\super9.tzx
    =================================================================
        768 Unter-Pfade gefunden   - identisch.
     15.214 Dateien     gefunden   - und verglichen...
     15.214 Dateien     verglichen - identisch.
          Es wurden 18.068.947.238 Bytes = 17.645.456 K-Bytes verglichen
          Start 13:09:19  Ende 13:09:56
    ========================================================<Ende>===
    Linke MausTaste / ESC = Schließen   Rechte MausTaste / F1 = Info
    

    Wenn ich statt der DllImport-Routinen die Puffer mit einer for-Schleife (ohne DllImport-Aufruf) vergleiche, dann dauert das auch etwa 30 Minuten.
    Mein Ziel war, das mit dem schon etwas betagtem Delph7 geschriebene Programm mit C# zu realisieren.
    Ich brauche das Programm, um nach den Sicherungen die gesicherten Daten mit den Originalen zu vergleichen, um einen Haken an die Sicherung zu machen.
    Der Puffervergleich mit Assembler ist wirklich extrem schnell, das wird von nur drei entscheidenden Befehlen gemacht. Eigentlich vergleicht nur der Befehl REPZ CMPSD die beiden Puffer von je 1 TB Länge. Man könnte jetzt auch 16 Byte lange Worte benutzen, da wäre es noch etwas schneller.

          CLD
          REPZ  CMPSD       // DWords vergleichen   ECX enthält die Anzahl DWORDS (8 Bytes)
          JNZ   Diff        // --> Differenz 
    

    Dagegen ist die C# Vergleichsroutine simpel und extrem langsam

                    for (int i = 0; i < VerglLen; i++)
                    {
                        if (ByPu1[i] != ByPu2[i]) // ungleiche Bytes gefunden ?
                        {
                            if (AnzDiff == 0) // Erste Differenz dieser Datei ?
                            {
                                sZei = sZei + "- ==> Unterschiede gefunden";
                                if (!Flag_F)
                                {
                                    PutListV(sZei);
                                }
                                sZei = "";
                            }
                            rc = 1; // es gibt Differenzen
                            isDiff = 1;
                            AnzDiff++;
                            GesDiff++; // Differenzen (Anzahl Bytes)
    
                            if (AnzDiff <= Anz_M)
                            {
                                ZeigeDiff(VerglOffset + i, ByPu1[i], ByPu2[i], VerglLen);
                            }
                        }
                    } // for (int i = 0; i < VerglLen; i++)
    

    Mein PC hat eine sehr schnelle CPU (AMD Ryzen 9 5900X), einen schnellen RAM (3200 MHz) und das benutze Laufwerk U: ist eine sehr schnelle m.2 (Samsung 980 PRO EVO). An der Hardware kann die gebremste Laufzeit nicht liegen.



  • Wir wissen nicht, wie der Vergleich in Delphi ausgeführt wird, in C# vergleichst du aber einzelne Bytes, das kann relativ teuer werden. Am schnellsten wird es wohl sein, wenn man Blöcke vergleicht, die der Registerbreite deiner CPU entsprechen (also 64bit). Wenn die unterschiedlich sind kann man sich im Detail angucken, wo sich die Blöcke tatsächlich unterscheiden.



  • Probiere mal direkt in C# (sofern du .NET Core 2.1 oder neuer, also z.B. NET 5 oder 6 benutzt):

    // byte[] is implicitly convertible to ReadOnlySpan<byte>
    static bool ByteArrayCompare(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2)
    {
        return a1.SequenceEqual(a2);
    }
    

    (u.a. aus Comparing two byte arrays in .NET)

    Wenn du aber bei deinem Assembler (bzw. C++) Code bleiben willst, dann könntest du auch ein C++/CLI-Projekt erstellen und dieses direkt als Referenz einbinden (aber auch hier wird ein Marshalling durchgeführt).



  • @DocShoe , in Delphi mache ich das mit den gleiche ASS-Befehlen, wie in C++. Und das klappt seit mehr als 20 Jahren so.

    { ----------------------------------------------------------------------
      -                     Puffer 1 und 2 vergleichen                     -
      ---------------------------------------------------------------------- }
    Function CompPuff : Boolean;
      Var
        RCode  : Byte;
      Begin
        asm
          PUSH  ESI
          PUSH  EDI
          PUSH  EBX
          PUSH  ECX
          PUSH  DS
          PUSH  SS
    
          XOR   EBX,EBX               { EBX löschen }
          MOV   [RCode],BL            { RCode löschen }
          MOV   ESI,Offset Puffer1
          MOV   EDI,Offset Puffer2
          MOV   ECX,[Laenge1]         { Länge in Bytes }
    
          MOV   BL,CL                 { Bit 0+1 in BL retten }
          SHR   ECX,2                 { Länge / 4 : in DWords }
          AND   BL,3                  { Restlänge = 0 ? }
          JZ    @CmpDW                { --> ja, nur DWords vergleichen }
    
          CLD
          REPZ  CMPSD                 { DWords vergleichen }
          JNZ   @Diff                 { --> Differenz }
          MOV   ECX,EBX               { Restlänge 1..3 }
          CLD
          REPZ  CMPSB                 { Restlänge vergleichen }
          JZ    @Ende
         @Diff:
          POP   SS
          POP   DS
          MOV   [RCode],1             { RCode = 1 : Differenz ! }
          PUSH  DS
          PUSH  SS
          JMP   @Ende
    
         @CmpDW:
          CLD
          REPZ  CMPSD                 { DWords vergleichen }
          JNZ   @Diff                 { --> Differenz }
    
         @Ende:
          POP   SS
          POP   DS
          POP   ECX
          POP   EBX
          POP   EDI
          POP   ESI
        end;
        if RCode = 1 then CompPuff := True     { Fehler }
                     else CompPuff := False;   { ok.    }
    
      End; { Function CompPuff }
    


  • @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    @DocShoe , in Delphi mache ich das mit den gleiche ASS-Befehlen, wie in C++. Und das klappt seit mehr als 20 Jahren so.

    Ich kann kein x86 Assembler, aber ich sehe, dass dort DWORDs und keine BYTEs verglichen werden, das ist zumindest ein Unterschied zwischen asm und C#. Man müsste mal schauen, wie der IL Code und letztendlich der ASM Code des C# Kompilats aussieht.



  • @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    REPZ CMPSD

    Dieser Befehl arbeitet so, wie eine ganze Befehlsfolge.
    zuvor CLD: wenn das D-Flag gelöscht ist, wird aufsteigen verglichen, andernfalls absteigend.
    ESI und EDI sind die Adressen der zu vergleichenden Puffer.
    Bei CMPSD bedeutet D, es werden Doppel-Worte = je 4 Bytes verglichen (CMPSB : es werden Bytes verglichen).
    Ist ein Vergleich gemacht und es gibt es keine Differenz, werden die Register ESI und EDI jeweils um 4 Bytes erhöht (auf das nächste Doppelwort im Puffer)
    Weiterhin wird die Anzahl der zu vergleichenden Doppelworte in ECX um 1 vermindert, wenn ECX dabei =0 wird, ist der Befehl beendet, andernfalls (ECX > 0) und REPZ vor CMPSD, dann findet der Vergleich des nächsten DWORD statt. REPZ bewirkt, das CMPSD so lange ausgeführt wird, bis entweder ECX = 0 wird oder eine Differenz auftritt.
    Falls eine Differenz erkannt wird, wird der Vergleich beendet (ESI und EDI stehen auf dem DWORD mit der Differenz) und der folgende JZ sprung wird nicht ausgeführt, "Z" ist nur dann der Fall, wenn ECX=0 und alles verglichenen DWORDs sind identisch



  • @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Hallo hustbaer, ich danke Dir für Deine Antworten.
    Ich denke aber, die deutlich längere Laufzeit liegt daran, das DLL bei jedem Aufruf neu geladen werden.

    Das ist sicher nicht so. Wenn du mir nicht glaubst, dann bau doch einfach ein DllMain in deine DLL ein und pack ein paar OutputDebugString rein. Dann siehst du selbst dass sie nur 1x geladen wird.

    Ich habe deshalb ein kleine C++ DLL gemacht, die lediglich eine Zahl weiterzählt.
    Und dazu ein C# Testrahmen, der eine Zählung mit und ohne diese DLL macht.
    Das Ergebnis: mit DLL = 65 Sekunden / ohne DLL = 1 Sekunde - als ca. das 65-fache an Laufzeit.

    D.h. der Overhead des PInvoke Aufrufs ist ca. 64x das was ein Schleifendurchlauf braucht der einen int addiert. Das muss dir doch selbst klar sein dass das mehrere Grössenordnungen davon entfernt ist was das Entladen+erneute Laden einer DLL brauchen würde. Und es ist auch völlig irrelevant wenn du pro Aufruf einen 1 GB Block kopierst. Ich meine du machst da 1 Mrd Aufrufe, vs. dein Backup/Verify-Programm wo du 18k Aufrufe machst.

    Aber mal was anderes: Wie lange braucht dein C# Programm denn wenn du die PInvoke Aufrufe einfach weglässt? Also wenn du einfach nur die Daten aus den beiden Files liest, ohne zu vergleichen.

    Der cl-Compiler erstellt ja auch eine obj-Datei, kann man die nicht an die C# EXE hinzu linken ?

    Nein, geht nicht.

    Man benutzt ja auch System-Routinen, die bestimmt nicht alle in C# programmiert sind.

    Ja. Über PInvoke. Also genau das DllImport von dem du fälschlicherweise annimmst es sei so langsam. Obwohl du dir eigentlich schon selbst bewiesen hast dass es sehr schnell ist (dein "Zaehle" Programm).



  • @hustbaer sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Wie lange braucht dein C# Programm denn wenn du die PInvoke Aufrufe einfach weglässt?

    Ich danke Dir für Deine Beharrlichkeit - Du hast Recht und ich ärgere mich, dass ich nicht selbst einmal diesen Versuch gemacht habe.
    Ohne jeden Vergleich läuft das Programm mit den gleichen Dateien 28' 10", d.h. der Vergleich spielt keine Rolle bei der Laufzeit und deshalb ist meine Vermutung DllImport sei der Bremser offenbar falsch - Du hattest Recht ☺ .
    Dann sind also die Zugriffsmethoden, die ich bei C# benutze im Vergleich zu denen in Delphi die Bremser.

    So mache ich das in C#
    Zunächst lese ich die beiden Dateien als Stream ein,
    später dann immer Blöcke aus dem Stream in einer max. Blocklänge von 1 TB, falls die Dateien > 1 TB sind.
    Diese Blöcke werden anschließend verglichen

                    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;
    
                    ByPu1 = new byte[(int)MaxPufL + 0x1000];
                    ByPu2 = new byte[(int)MaxPufL + 0x1000];
    
                NxtPuVergleich:
    
                    if (VerglRestLen > MaxPufL)
                    { VerglLen = (int) MaxPufL; }
                    else
                    { VerglLen = (int) VerglRestLen; }
    
                    fs1.Read(ByPu1, 0, (int)VerglLen);
                    fs2.Read(ByPu2, 0, (int)VerglLen);
    

    Hier Schnipsel des Delphiprogrammes

           {$I-}
            AssignFile(Handle1, DSN1);
            FileMode := 0;    { Reset nur für Eingabe }
            Reset(Handle1,1);
            Error1 := IOResult; { Null = Open OK }
            {$I+}
            :::
            {$I-}
            BlockRead(Handle1, Puffer1, ReadLen, Laenge1);
            Error1 := IOResult; { Null = Read OK }
            if Error1 <> 0 then
              Laenge1 := 0;
            {$I+}
    

    Das sind im Prinzip die Zugriffe, die es bereits unter DOS gab und die ich in Borlands Turbo Pascal schon benutzt habe.
    Die kann man im aktuellen Delphi 10 aber auch noch benutzen.

    Ich muss nun prüfen, ob man so elementare Zugriffe zum simplen sequentiellen byteweisen lesen von Blöcken einer beliebigen Datei auch in C# machen kann. Die stream-Methode ist für so ein Programm wahrscheinlich völlig ungeeignet. Bei C++ gibt es auch noch diese ursprünglichen DOS-Zugriffe.



  • Wenn du´s wirklich ausreizen möchtest kannst du ja mal diesen Ansatz ausprobieren:

    1. beide Dateien mit OpenFile öffnen
    2. FileMapping für beide Dateien erzeugen (CreateFileMapping)
    3. blockweise jeweils die gleichen X Byte pro Datei in den Speicher einblenden (MapViewOfFile)
    4. Speicherbereiche per RtlCompareMemory vergleichen (Vorsicht: undokumentierte Funktion)
    5. Blockgröße und Offset anpassen, weiter bei 3) oder Ende, falls Dateiende erreicht

    Vielleicht kann Windows da intern etwas optimieren.



  • Für .NET gibt es dafür die Klasse MemoryMappedFile, s.a. Benchmark-Vergleich in C# MemoryMappedFile Example.

    PS:
    @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    später dann immer Blöcke aus dem Stream in einer max. Blocklänge von 1 TB, falls die Dateien > 1 TB sind.

    Du meinst GB??!



  • @Th69

    Ich wusste nicht, dass es sowas in .NET gibt. Aber meine Absicht war eigentlich so viel wie möglich nicht in C# zu machen, sondern möglichst viel von der WINAPI direkt erledigen zu lassen. Mit der MemoryMappedFile Klasse braucht man ja schon wieder .NET Streams, während RtlCompareMemory direkt mit Adressen arbeitet.



  • @Th69 sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Du meinst GB

    ja, natürlich

    Wie ist das eigentlich bei File.ReadAllBytes
    https://learn.microsoft.com/de-de/dotnet/api/system.io.file.readallbytes?view=net-6.0
    In dem Link werden die möglichen Ausnahmen aufgezeigt.
    Was passiert aber, wenn die Datei so groß ist, dass dafür kein byte[] Array im verfügbaren Speicher angelegt werden kann.
    Muss man das byte[] Array selbst wieder freigeben ?
    Ich benutze deshalb die max.Puffergröße von 1 GB, bei meinem derzeitigen RAM von 32 GB könnte es auch etwas mehr sein.
    Auf meinem PC gibt es aber Dateien, die deutlich größer als 1 TB sind.
    Eine Ausnahme sinngemäß "Datei für vorhandenen Speicher zu groß" habe ich nicht gefunden.

    07.09.2022  09:39    18.353.100.330 2022-09-06 xxx HD.mp4
    08.09.2022  16:22    17.293.742.949 2022-09-07 xxx HD.mp4
    09.09.2022  10:37    17.293.367.198 2022-09-08 xxx HD.mp4
    10.09.2022  07:32     8.115.634.344 2022-09-09 xxx HD.mp4
    22.09.2022  08:38     9.881.955.469 2022-09-21 xxx HD.mp4
    23.09.2022  07:25    15.526.817.738 2022-09-22 xxx HD.mp4
                   6 Datei(en), 86.464.618.028 Bytes
    


  • @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    @Th69 sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:
    Wie ist das eigentlich bei File.ReadAllBytes
    https://learn.microsoft.com/de-de/dotnet/api/system.io.file.readallbytes?view=net-6.0
    In dem Link werden die möglichen Ausnahmen aufgezeigt.
    Was passiert aber, wenn die Datei so groß ist, dass dafür kein byte[] Array im verfügbaren Speicher angelegt werden kann.

    Dann fliegt eine OutOfMemoryException.

    Muss man das byte[] Array selbst wieder freigeben ?

    Nö. Dafür gibt's ja garbage collection. Nur bringst du den GC halt mächtig unter Druck wenn du alles über Funktionen einliest die immer neue Arrays erzeugen.

    Ich benutze deshalb die max.Puffergröße von 1 GB, bei meinem derzeitigen RAM von 32 GB könnte es auch etwas mehr sein.
    Auf meinem PC gibt es aber Dateien, die deutlich größer als 1 TB sind.

    Wenn du ein grosses Pagefile hast bzw. die "automatisch verwalten" Einstellung, dann wird das vermutlich erstmal richtig langsam werden, und dann wird irgendwann trotzdem eine OutOfMemoryException fliegen.

    Und wenn du kein/nur ein kleines Pagefile hast, dann wird es nicht so lange dauern bis die OutOfMemoryException geflogen kommt 🙂

    Eine Ausnahme sinngemäß "Datei für vorhandenen Speicher zu groß" habe ich nicht gefunden.

    Ja, OutOfMemoryException kann von so vielen Funktionen geworfen werden dass sie das nicht extra dokumentieren.



  • ps:
    @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

                FileStream fs1 = new FileStream(Dsn1, FileMode.Open, FileAccess.Read);
                FileStream fs2 = new FileStream(Dsn2, FileMode.Open, FileAccess.Read);
    

    Ich würde empfehle das mit using zu machen, damit die Files auch garantiert immer wieder zugemacht werden. Ohne using kann es u.U. lange dauern bis die FileStream Objekte vom GC weggeräumt werden - und bis dahin bleibt das File-Handle dann offen.

    using (FileStream fs1 = new FileStream(Dsn1, FileMode.Open, FileAccess.Read))
    using (FileStream fs2 = new FileStream(Dsn2, FileMode.Open, FileAccess.Read))
    {
    
        // ...
    
    } // fs1 und fs2 werden hier automatisch geschlossen, egal wie der Block verlassen wird (normal, return, Exception)
    


  • @hustbaer sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Ich würde empfehle das mit using zu machen

    Ich mache doch am Ende in jedem Fall folgende Close-Aufrufe - ist das nicht identisch ?

                    fs1.Close();
                    fs2.Close();
    
    

    Bei der Anwendung meines Programmes ist es ja so, dass zwar viele Dateien und Bytes verglichen werden, es dabei i.d.R. aber weder Lesefehler noch Differenzen gibt. Dor Standard ist als alle Dateien sind OK und stimmen überein.
    Trotzdem lasse ich nach Sicherungen meiner wichtigen Dateien (meist auf externe HDDs) danach diesen Vergleich laufen. Damit habe ich eine Lesekontrolle und die Sicherheit, dass alles stimmt.



  • @hustbaer sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Dann fliegt eine OutOfMemoryException.

    Damit ist diese Funktion nur für relativ kleine Dateien nutzbar.



  • @hkdd sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    @hustbaer sagte in Wird bei DllImport die DLL-Datei pro Aufruf geladen ?:

    Ich würde empfehle das mit using zu machen

    Ich mache doch am Ende in jedem Fall folgende Close-Aufrufe - ist das nicht identisch ?

                    fs1.Close();
                    fs2.Close();
    
    

    Bei der Anwendung meines Programmes ist es ja so, dass zwar viele Dateien und Bytes verglichen werden, es dabei i.d.R. aber weder Lesefehler noch Differenzen gibt. (...)

    Mir ist das im Prinzip Wurst wie du das machst 🙂 Ich wollte dich nur darauf hinweisen dass man das üblicherweise in .NET halt nicht so macht. Sondern eben using verwendet. Weil es eben nicht identisch ist. Deine Close Aufrufe werden halt nur erreicht wenn keine Exception fliegt. Die using Blöcke machen die Files dagegen auch in dem Fall zuverlässig zu.

    Damit ist diese Funktion nur für relativ kleine Dateien nutzbar.

    Ich würde ein paar GB schon als relativ gross bezeichnen. Ist halt relativ 😄


Log in to reply