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.
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.
Der cl-Compiler erstellt ja auch eine obj-Datei, kann man die nicht an die C# EXE hinzu linken ?Ich compiliere die DLL im x86 Native Tools Command Prompt for VS2022 Fenster mit folgendem Kommando
U:\c\1>cl Zaehle.cpp /LD /EHsc Microsoft (R) C/C++-Optimierungscompiler Version 19.33.31630 für x86 Copyright (C) Microsoft Corporation. Alle Rechte vorbehalten. Zaehle.cpp Microsoft (R) Incremental Linker Version 14.33.31630.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:Zaehle.dll /dll /implib:Zaehle.lib Zaehle.obj Bibliothek "Zaehle.lib" und Objekt "Zaehle.exp" werden erstellt. U:\c\1>
So sieht die DLL aus
//Zaehle.cpp #include <stdio.h> extern "C" __declspec(dllexport) int Zaehle(int Zahl) { return Zahl + 1; }
und so das C# Testprogramm dazu
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace TestDLL { internal class Program { static string displayTime() // gibt zeit + datum zurück { string time = null; time += System.DateTime.Now.Hour + ":"; time += System.DateTime.Now.Minute + ":"; time += System.DateTime.Now.Second; return time; } [DllImport(@"U:\c\1\Zaehle.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Zaehle(int Zahl); static void Main(string[] args) { int m = 1000000000; Console.WriteLine("START mit DLL - " + displayTime()); int i = 0; while (i < m) { i = Zaehle(i); } // in DLL zählen Console.WriteLine("ENDE mit DLL - " + displayTime() + " Anzahl Aufrufe= " + i.ToString()); Console.WriteLine("START ohne DLL - " + displayTime()); i = 0; while (i < m) { i++; } // ohne DLL Console.WriteLine("ENDE ohne DLL - " + displayTime() + " Anzahl Aufrufe= " + i.ToString()); Console.ReadKey(); } /* E R G E B N I S : START mit DLL - 10:54:42 ENDE mit DLL - 10:55:47 Anzahl Aufrufe= 1000000000 = 65 Sekunden START ohne DLL - 10:55:47 ENDE ohne DLL - 10:55:48 Anzahl Aufrufe= 1000000000 = 1 Sekunde */ } }
-
Rufe mal nur einmalig die DLL-Funktion auf und messe dessen Zeit (am besten per
Stopwatch
in Millisekunden bzw. Ticks), dies ist dann die zusätzliche Zeit für das Laden der DLL.Aber jeder
DllImport
-Aufruf ist wegen des zusätzlichen Marshallings zeitaufwendiger als ein interner Methodenaufruf.
Du solltest daher die externen Aufrufe begrenzen, d.h. wenn du eine Schleife benötigst, dann sollte diese komplett in der DLL stattfinden.PS: Weil du danach gefragt hast: der Aufruf mittels
DLLImport
ist P/Invoke.
-
@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/Linker22.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 paarOutputDebugString
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 verglichenFileStream 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:
- beide Dateien mit OpenFile öffnen
- FileMapping für beide Dateien erzeugen (CreateFileMapping)
- blockweise jeweils die gleichen X Byte pro Datei in den Speicher einblenden (MapViewOfFile)
- Speicherbereiche per RtlCompareMemory vergleichen (Vorsicht: undokumentierte Funktion)
- 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??!
-
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 kommtEine 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. Ohneusing
kann es u.U. lange dauern bis dieFileStream
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.