Programmierbuddies C++/Boost/C++11

eul3r

Member
Hallo,

suche interessierte C++ Programmierer :-)

Für meinen FreeBSD HomeServer habe ich ein einfaches Projekt gestartet.
Ein Datei Doubletten Auffinder.
Es gibt hier schon ein Programm z.B. fdupes.
Das ist aber ziemlich langsam und bei > 100k Dateien dauert, das gefühlt Jahre.

Generelle Programm Beschreibung:
Finde alle Datei Doubletten unter dem angegebenen Pfad unter der Verwendung von x Threads.


Verwendung von:
CMake
Clang/GCC
C++11 Threads, Packaged_Task
Boost Filesystem
Cryptopp


Bin offen für neue Ideen, spannend sind hier natürlich Algorithmen, Filesystem Wissen, etc.

Bei interesse einfach melden :-)
 
Warum denkst du das C++ dafür das richtige ist? Vielleicht nicht lieber shell und Prüfsummen tools?
 
Warum versuchst du nicht, ein bereits existierendes Programm zu verbessern?

Rob

Habe mir die Programmlandschaft angeschaut, und bei C++ ist sie dünn. Modernes C++ wird nicht eingesetzt.
Viele Programme leiden unter Feature-ities. Ich möchte ein simples einfaches Programm nach UNIX Philosopie.

Warum denkst du das C++ dafür das richtige ist? Vielleicht nicht lieber shell und Prüfsummen tools?
Durch Benchmarking sieht man die Unterschiede. Es liegt einfach an den Algorithmen. Die Shell ist dafür nicht geeignet, siehe übliche Shell Literatur.


Beispiel: 100k Files zwischen 40 B - 4 MB
fdupes: ~ 30+ Sekunden
Mein Programm: 2,8 Sekunden

Ich suche außerdem noch Betatester für > 1 Millionen Files, ob der Algorithmus auch skaliert.
 
Wenn du Mitstreiter oder Tester suchst dann solltest du mal einen Link zu deinem Projekt anbieten.
 
Das kann man mit einem recht einfachen Shell-Skript basteln. Kein Grund da groß Entwicklungsaufand reinzustecken.
 
Das kann man mit einem recht einfachen Shell-Skript basteln. Kein Grund da groß Entwicklungsaufand reinzustecken.

Hier ein Beispiel Shell Script:
// Skript Quelle: http://www.commandlinefu.com/comman...icate-files-based-on-size-first-then-md5-hash
find -not -empty -type f -printf "%s\n" | sort -rn | uniq -d | xargs -I{} -n1 find -type f -size {}c -print0 | xargs -0 md5sum | sort | uniq -w32 --all-repeated=separate


Für Ordner mit 30 Subordner und paar Hundert Dateien läuft es schnell.
Hingegen für > 100k Dateien ist es super langsam und letztendlich unpraktikabel ( Nach 5 Minuten hab ich das abgebrochen...).
Wie oben schon gesagt das vorhandene Programm fdupes benötigt ca. ~30+ Sekunden.

Es geht um die benötigte Zeit für große Datenmengen und Skalierbarkeit. Konnte bis jetzt nichts passables finden.


Schöne Grüße.
 
Der Ansatz der Zeile um find(1) ist schon recht gut:

1) Treffe eine Vorauswahl der Dateien Anhand ihrer Länge.
2) Vergleiche Dateien mit gleicher Länge mittels eines Hash-Algos.

Das erste Problem der Zeile, dass find(1) auf nur einem Thread läuft. Da die meiste Zeit für die stat() Syscalls draufgehen dürfte, ist es ineffizient. Außerdem sollte bei besseren Dateisystem eh immer mindestens ein ganzes Verzeichnis in den Cache gelesen werden, wodurch nach dem ersten Zugriff keine weiteren Plattenzugriffe folgen dürften. In C würde man z.B. fts() per pthreads parallel auf Teilbäume abfeuern, in sh könnte man z.B. was mit temporären Dateien frickeln. sort(1) ist ebenfalls nicht für seine Geschwindigkeit begannt, sortieren ist halt teuer. Aber man muss auch gar nichts sortieren, stattdessen könnte man die Längen der Dateien als Schlüssel für eine Hashmap nehmen. In Bash könnte man dafür assoziative Arrays missbrauchen. Die gleich langen Dateien zu Hashen ist der mit Anstand aufwändigste Schritt. Das ist natürlich wunderbar parallelisierbar, vor allem sollte man aber einen schnelleren Hash-Algo als das grottenlahme (und für solche Zwecke nicht gedachte!) md5 nehmen. In C könnte man da die paar Zeilen für den guten, alten Bob Jenkins Hash fallen lassen, mit Shell-Scripten wird es in Ermagelung nicht-kryptografischer Hash-Tools etwas problematischer.

Was aus einigen hochoptimierten C-Programmen und etwas Shell-Script zusammenklebbar ist, zeigt unter anderem das Universal Shell Programming Laboratory (http://www.usp-lab.com) mit seinem Unicage-System. Ich habe das mal Live gesehen, es ist schon sehr krass und verliert den Glauben an hochkomplexe, angeblich bis zu erbrechen optimierte Spezialanwendungen. Hier der BSDCan-Vortrag, der einen guten Einstieg bietet: http://www.bsdcan.org/2013/schedule/attachments/244_UnicageDevelopment.pdf

Aber egal, das war nun alles etwas offtopic :)
 
Ich bin nicht sicher, ob Yamagi mit der Hashmap das gleiche im Sinn hat wie ich, es hört sich ähnlich an.
Ich würde den Verzeichnisbaum durchgehen und zu jeder Datei zwei Informationen in eine Liste einsortieren, Größe und Name (incl. Pfad). "Einsortieren" soll heißen, nach Größe sortiert. Komplexität n·log n, n fürs Durchgehen, log n fürs Einsortieren. Dann geht man diese Liste durch (Komplexität n) und vergleicht Einträge gleicher Größe (Komplexität nur schwach abhängig von n). Beim Vergleichen von nur zwei (drei?) Dateien ist ein binäres "diff" (oder dasselbe "zu Fuß" im C-Code) wohl am schnellsten, bei größerer Zahl von möglichen Duplikaten bieten sich Hash-Verfahren an, aber möglichst primitive, z. B. durchgehendes XOR aller 64-bit-Stücke. (Oder Bob Jenkins, ich bin jetzt zu faul, um das nachzuschlagen.)
Wichtig ist, daß der Dateiinhalt nur bei den Dateien überhaupt gelesen wird, die einen "Partner in der Größe" haben.
Die verwendete Liste muß sortiert sein, da sonst beim Suchen nach Einträgen gleicher Größe eine Komplexität n² auftritt.
 
Ich bin nicht sicher, ob Yamagi mit der Hashmap das gleiche im Sinn hat wie ich, es hört sich ähnlich an.
Ich würde den Verzeichnisbaum durchgehen und zu jeder Datei zwei Informationen in eine Liste einsortieren, Größe und Name (incl. Pfad). "Einsortieren" soll heißen, nach Größe sortiert. Komplexität n·log n, n fürs Durchgehen, log n fürs Einsortieren. Dann geht man diese Liste durch (Komplexität n) und vergleicht Einträge gleicher Größe (Komplexität nur schwach abhängig von n). Beim Vergleichen von nur zwei (drei?) Dateien ist ein binäres "diff" (oder dasselbe "zu Fuß" im C-Code) wohl am schnellsten, bei größerer Zahl von möglichen Duplikaten bieten sich Hash-Verfahren an, aber möglichst primitive, z. B. durchgehendes XOR aller 64-bit-Stücke. (Oder Bob Jenkins, ich bin jetzt zu faul, um das nachzuschlagen.)
Wichtig ist, daß der Dateiinhalt nur bei den Dateien überhaupt gelesen wird, die einen "Partner in der Größe" haben.
Die verwendete Liste muß sortiert sein, da sonst beim Suchen nach Einträgen gleicher Größe eine Komplexität n² auftritt.

Den Grundlegenden Algorithmus hast du gut beschrieben. Allerdings kann nicht jedes x beliebige Hashing Verfahren verwendet werden, Kollisionen sind zu beachten.
Von daher wird ein MD5 Hash verwendet.



Meine Frage wäre noch wie verpacke ich das Programm jetzt am besten?
Option 1: Als CMake Projekt (es werden GCC4.9 - alternativ clang 3.3, Boost 1.5.8, Cryptopp benötigt)
Option 2: Binary mit dem Libs als tar Archiv
 
Wenn du Angst vor Kollisionen hast (ich würde das bei einer halbwegs brauchbaren Hash-Funktion für vernachlässigbar halten), könntest du die möglichen Duplikate auch durch einen Binary-Diff Algo jagen. Einfach Block für Block diffen und bei der ersten nicht leeren Ergebnismenge abbrechen. Da gibt sehr schnelle, effiziente Algorithmen. Der gute alte Xdelta ist beispielweise schnell implementiert und halbwegs okay. Colin Percival hat mehrere Algos entwickelt, wovon bsdiff wohl der für die meisten Zwecken beste Kompromiss aus Geschwindigkeit und Ekligkeit der Implementierung ist: http://www.daemonology.net/bsdiff/
 
Wenn du Angst vor Kollisionen hast (ich würde das bei einer halbwegs brauchbaren Hash-Funktion für vernachlässigbar halten), könntest du die möglichen Duplikate auch durch einen Binary-Diff Algo jagen. Einfach Block für Block diffen und bei der ersten nicht leeren Ergebnismenge abbrechen. Da gibt sehr schnelle, effiziente Algorithmen. Der gute alte Xdelta ist beispielweise schnell implementiert und halbwegs okay. Colin Percival hat mehrere Algos entwickelt, wovon bsdiff wohl der für die meisten Zwecken beste Kompromiss aus Geschwindigkeit und Ekligkeit der Implementierung ist: http://www.daemonology.net/bsdiff/

Danke für die Info, werde ich mal ausprobieren. Mein Bauchgefühl sagt mir, dass für mehrere Dateienvergleiche via Binary Diff die Performance flöten geht.



Beispiel Benchmark 1 - Server:
Intel Avoton CPU C2758 @ 2.40GHZ auf RaidZ2 (WD RED) -> 8 cores
32GB RAM
aktuelles FreeNAS

Programm: (FILES 24.666)
fdupes: 1 Minute 9 Seconds
Mein Programm: 11 Seconds


Beispiel Benchmark 2 - Notebook: (Files: 173.229)
Intel Core i7-3740QM CPU @ 2.70GHz -> 4 cores + HT auf SSD
8GB RAM
aktuelles ArchLinux

Programm: (Files: 173.229)
fdupes: 39 Seconds
Mein Programm: 9 Seconds
 
Bin offen für neue Ideen, spannend sind hier natürlich Algorithmen, Filesystem Wissen, etc.

Bei der Prüfung auf Dateien mit gleichem Inhalt kannst du Zeit sparen, wenn du auch die Existenz von Hardlinks berücksichtigst. In dem Fall ist der Inhalt nicht nur der gleiche, sondern der selbe.

Du hattest noch nicht geschrieben, wofür du das Programm brauchst. Wenn es um Deduplizierung gleicher Datenblöcke geht, kannst du dir auch ZFS ansehen, das die ganze Geschichte für dich übernimmt.

Threads würde ich auf Hardware aufteilen, wenn du getrennte Medien im Verzeichnisbaum eingehängt hast. Spätestens wenn du bei den Vergleichen hohe I/O-Last hast, bringt es relativ wenig, mehrere Threads im BIO-Wait zu haben.

in eine Liste einsortieren, Größe und Name (incl. Pfad). "Einsortieren" soll heißen, nach Größe sortiert. Komplexität n·log n, n fürs Durchgehen, log n fürs Einsortieren.

Ich habe mir die erwähnten Bibliotheken nicht angesehen, aber wenn Heaps vorhanden sind, wäre auch ein Min- oder Max-Heap interessant. Da diese aber auch als Liste repräsentiert werden können, ist das nur eine kleine Anmerkung. Ebenfalls O(n * log n), daher ist es eine Implementationsfrage.

Wenn sich die Spannweite der Dateigrößen im Rahmen hält, könntest du auch einen Counting Sort (oder Radix Sort) betreiben, der das mit O(n) lösen kann. Aber dann wird dein Programm schnell sehr speziell und nicht mehr sonderlich allgemeingültig.
 
Du hattest noch nicht geschrieben, wofür du das Programm brauchst. Wenn es um Deduplizierung gleicher Datenblöcke geht, kannst du dir auch ZFS ansehen, das die ganze Geschichte für dich übernimmt.
Das ist viel zu teuer...5GB pro TB laut ZFS Tuning Guide....
Allein ein Board mit 64 GB RAM ist schon recht teuer.

Threads würde ich auf Hardware aufteilen, wenn du getrennte Medien im Verzeichnisbaum eingehängt hast. Spätestens wenn du bei den Vergleichen hohe I/O-Last hast, bringt es relativ wenig, mehrere Threads im BIO-Wait zu haben.
Das habe ich schon gebenchmarkt. Man kann mit mehr Threads als Logischen Prozessoren( Cores *2 [HT]) doch noch ein paar Sekunden rauskitzeln.

Wenn man die Ausgabe in eine Datei umlenkt spart das nochmals 6 Sekunden.

Beispiel Benchmark 2 - Notebook: (Files: 173.229)
Intel Core i7-3740QM CPU @ 2.70GHz -> 4 cores + HT auf SSD
8GB RAM
aktuelles ArchLinux

Programm: (Files: 173.229)
fdupes: 39 Seconds
Mein Programm: 2.9
Seconds bei 23 Threads => Faktor 13.5 schneller als FDupes
Mein Programm: 3.7 Seconds bei 8 Threads (optimal für Hardware 4 Cores + HyperThreading => 8)
 
Hast du auch überprüft, ob die Ergebnisse stimmen?
Rob

Ja.

Wer mal reinschauen möchte es ist jetzt online:
https://github.com/eul3er/bdups

Hinweise:
cmake installieren
boost installieren
cryptopp installieren
GCC4.9 oder clang 3.4 installieren

cmake $PfadZumOrdnerDerCMakeLists.txt
make -j8

Fertig.

jetzt kann ausprobiert werden
Als erstes die Anzahl der Threads übergeben, bei 0 wird die optimale Anzahl vom Programm ermittelt.
./fdupes 30 /home/meinOrdner
 
So ich habe mal reingeschaut in den Code, zumindest die wichtig aussehenden Teile.
Ein wenig Feedback:

Coding Style:

Da sind einige Blüten drin wie hier, wo Du einen Iterator auf einen vector übergibst an findDuplicatesFileSize. Soweit so klar, man soll wohl in der Kollektion, die übergeben wird, nach Duplikaten nach Dateigröße suchen. Das machst du mit std::adjacent_find, aber diese Funktion bekommt Iterator für Anfang und Ende des zu durchsuchenden Bereiches. Das Ende holst du aber immer aus dem Vector m_duplicates. Das heißt, die Funktion funktioniert *nur*, wenn du sie mit m_duplicates.begin() als Parameter aufrufst. Da Du sie nur ein einziges mal verwendest, klappt das in deinem Code.

Etwas anderes sind Kommentare und Namensgebung. Vermeide Funktionen wie "DoWork()". Niemand außer Du weiß was diese Funktion tut, außer dass sie anscheinend überhaupt etwas tun soll. Versuche auf alle Fälle den Informationsgehalt von Namen über 0 zu halten. Bei Kommentaren scheint manchmal X zu stehn und Y getan zu werden.


Du schreibst, dein Programm soll skalieren können für viele Dateien. Ich sehe da auf Anhieb zwei Probleme:

Das größte: Du nutzt Rekursion, und zwar einen Call pro Duplicate-Dateigröße. Kopier zweimal ein Maildir (so dass jede Datei zweimal existiert, also ein Duplikat ist) und Du wirst einen Stackoverflow kriegen - es sei denn dein Compiler macht dort TCO. Das klappt aber nicht Compilerübergreifend und erst recht nicht mit sicheren Optimierungsstufen.
Das kleinere: Du liest alle Dateien mit Größe erstmal in den RAM und sortierst diese dann. Das sind O(n) Speicherauslastung und O(n*log n) Rechenzeit. Durch deine Verwendung einer Hash-Funktion bei Duplikaten kommst Du nochmal auf O(ds) wenn ds die Dateigröße aller Duplikate zusammen ist. Ein Delta wäre hier schneller, wenn nur zwei Kandidaten zu vergleichen sind.

Zur Korrektheit: Deine Verwendung von unique_ptr durch vielen Code hinweg scheint mir unsinnig. Da entstehen am Ende unzählige Aufrufe zu std::move, um das abzuangen. Bei manchen Calls bin ich mir sogar unsicher, ob die überhaupt funktionieren. Lass mal valgrind --tool=memcheck ./yourprog <threads> <path> drüber laufen und schau ob da was meckert. Diese Funktion etwa tut nichts sinnvolles. Schau die mal shared_ptr und const myvar& an.

Hoffe das Feedback hilft etwas. :-)
 
Ich muss @namor Recht geben.

Wrapper um std::move() sind sinnlos. Mit Deinen Funktionen erzwingst Du ein Copy. Da Du das erzwungen hast, bringt dir ein nachträglicher Versuch das in Move-Semantik zu wandeln auch nichts. Rückgabewerte sind sowieso immer Rvalues, da der Kontext aus dem sie kommen ja nicht mehr existiert. Std::move ist nur ein Workaround für Sonderfälle in denen Du ein Move machen willst aber weiterhin Zugriff auf das ursprüngliche Objekt hast.

Ein "return std::move(…)" ist also immer sinnlos. Ohne Ausnahme. Du machst das sehr oft.

Was std::unique_ptr angeht, ist Deine Verwendung auch sehr seltsam. Alles was Du an Deine select_in() Methode gibst wird gelöscht. Wozu? Das kann man doch dem Aufrufer überlassen. Das ist deutlich übersichtlicher. Abgesehen davon unterstützt std::vector schon Move-Semantik. Da muss man kein uniqu_ptr drumrumpfriemeln. Wenn Du Move-Semantik willst sollte die Methode so definiert sein:
Code:
select_in(std::vector<CFileEntry> && data)

Der Aufrufende müsste dann ggf. std::move verwenden. Dafür gibt es aber gar keinen Grund. Übergib einfach eine Referenz und gut ist:
Code:
select_in(std::vector<CFileEntry> & data)

Und da Du den nicht zu verändern scheinst mach ihn gleich noch const:
Code:
select_in(std::vector<CFileEntry> const & data)

Weiter geht es mit folgender Zeile:
Code:
std::unique_ptr<std::vector<CFileEntry>> input_selection(new std::vector<CFileEntry>);

Das gehört so:
Code:
auto input_selection = std::vector<CFileEntry>{};
Oder äquivalent:
Code:
std::vector<CFileEntry> input_selection{};

Auch hier gibt es keinen Grund einen unique_ptr drumruzupfriemeln oder das auf den Freestore (Heap) zu legen.

Gleiches gilt natürlich auch für select_out.
 
Etwas anderes sind Kommentare und Namensgebung. Vermeide Funktionen wie "DoWork()". Niemand außer Du weiß was diese Funktion tut, außer dass sie anscheinend überhaupt etwas tun soll. Versuche auf alle Fälle den Informationsgehalt von Namen über 0 zu halten. Bei Kommentaren scheint manchmal X zu stehn und Y getan zu werden.
Akzeptiert. Werde ich verbessern.

Das größte: Du nutzt Rekursion, und zwar einen Call pro Duplicate-Dateigröße. Kopier zweimal ein Maildir (so dass jede Datei zweimal existiert, also ein Duplikat ist) und Du wirst einen Stackoverflow kriegen - es sei denn dein Compiler macht dort TCO. Das klappt aber nicht Compilerübergreifend und erst recht nicht mit sicheren Optimierungsstufen.
Guter Punkt, werde ich ändern.

Das kleinere: Du liest alle Dateien mit Größe erstmal in den RAM und sortierst diese dann. Das sind O(n) Speicherauslastung und O(n*log n) Rechenzeit. Durch deine Verwendung einer Hash-Funktion bei Duplikaten kommst Du nochmal auf O(ds) wenn ds die Dateigröße aller Duplikate zusammen ist. Ein Delta wäre hier schneller, wenn nur zwei Kandidaten zu vergleichen sind.
Ok, nehme ich auf die Optimierungsliste auf.

Zur Korrektheit: Deine Verwendung von unique_ptr durch vielen Code hinweg scheint mir unsinnig. Da entstehen am Ende unzählige Aufrufe zu std::move, um das abzuangen. Bei manchen Calls bin ich mir sogar unsicher, ob die überhaupt funktionieren. Lass mal valgrind --tool=memcheck ./yourprog <threads> <path> drüber laufen und schau ob da was meckert. Diese Funktion etwa tut nichts sinnvolles. Schau die mal shared_ptr und const myvar& an.

Sehe etwas differenzierter. Unique_ptr ist für unique Ownership. Die Daten werden durchgereicht und nicht an mehreren Stellen benötigt. Beispielsweise bei einer autom. Speicherfunktion wäre shared_ptr sinnvoll oder bei Nutzung von Threads.

Würde man std::move weglassen bekommt man ein Compiler Fehler. Da bei einem unique_ptr bekanntlich der Cpy-Ctor = delete ist.
Ich hatte es anfangs ohne unique_ptr und habe den vector direkt als rvalue übergeben. Ergebnis war, dass das Programm gecrasht ist. Mit Unique_ptr klappt es.
Warum? Habe es nicht weiter verfolgt.


Hier die Valgrind output:
==1508== Memcheck, a memory error detector
==1508== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==1508== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==1508== Command: ./bdups 33 /mnt/hdd/TestFolder/
==1508==
==1508==
==1508== HEAP SUMMARY:
==1508== in use at exit: 72,704 bytes in 1 blocks
==1508== total heap usage: 5,273,546 allocs, 5,273,545 frees, 2,266,388,354 bytes allocated
==1508==
==1508== LEAK SUMMARY:
==1508== definitely lost: 0 bytes in 0 blocks
==1508== indirectly lost: 0 bytes in 0 blocks
==1508== possibly lost: 0 bytes in 0 blocks
==1508== still reachable: 72,704 bytes in 1 blocks
==1508== suppressed: 0 bytes in 0 blocks
==1508== Rerun with --leak-check=full to see details of leaked memory
==1508==
==1508== For counts of detected and suppressed errors, rerun with: -v
==1508== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
 
Konnte den vorherigen Beitrag leider nicht editieren.

Dateien werden nicht eingelesen, sondern nur der Pfad und die Dateigröße.
Die Datei wird nur dann eingelesen, falls es zu einem Hash kommt.
Hier könnte man noch Blockweise vergleiche einführen, um auch größere Dateien performant vergl. zu können.


Ich danke für die Kritik.
 
Sehe etwas differenzierter. Unique_ptr ist für unique Ownership. Die Daten werden durchgereicht und nicht an mehreren Stellen benötigt. Beispielsweise bei einer autom. Speicherfunktion wäre shared_ptr sinnvoll oder bei Nutzung von Threads.

Würde man std::move weglassen bekommt man ein Compiler Fehler. Da bei einem unique_ptr bekanntlich der Cpy-Ctor = delete ist.
Ich hatte es anfangs ohne unique_ptr und habe den vector direkt als rvalue übergeben. Ergebnis war, dass das Programm gecrasht ist. Mit Unique_ptr klappt es.
Warum? Habe es nicht weiter verfolgt.
Du hast den vector auf den Freestore (Heap) gelegt, was vollkommen unnötig ist. Es gibt hier wirklich keinen Grund da etwas anderes als eine const Referenz zu übergeben.
 
Du hast den vector auf den Freestore (Heap) gelegt, was vollkommen unnötig ist. Es gibt hier wirklich keinen Grund da etwas anderes als eine const Referenz zu übergeben.

Ok, ich habe verstanden - es stört dich. Es ändert nichts an dem Fakt das beide Varianten Äquivalent sind. Der Output ändert sich nicht und die Performance auch nicht wesentlich, wobei diese mit time sehr schwer zu messen ist. Von daher ist die Kritik nicht hilfreich, Danke:).
 
Meine Variante ist schneller. Das spielt hier keine Rolle aber was Du da machst ist von hinten durch die Brust ins Auge.

Du hast offensichtlich nicht verstanden wie RAII funktioniert.
 
Zurück
Oben