SHA-2 in C++



  • Soo, nachdem ich mich aus gegebenem Anlass dann doch mal dazu überwinden konnte etwas älteren Code aufzuräumen, stelle ich mich jetzt der allgemeinen Steinigung:
    https://github.com/cooky451/sha2pp



  • Soll ich "kein Kommentar" jetzt als gut oder schlecht werten? 🤡



  • Kommen da noch Tests, Beispiele oder Benchmarks?



  • Die Geschwindigkeiten stehen ja als Kommentar im Header:

    * Speeds with VS 2012 on a 3570K @3.4GH:
     * x86: SHA256: ~190 MB/s, SHA512: ~70 MB/s
     * x64: SHA256: ~205 MB/s, SHA512: ~312 MB/s
    

    Beispiele, na ja:

    string s = "Hallo, Welt!";
    auto hash1 = sha256(s.data(), s.size());
    
    sha256_hasher;
    hasher.update(s.data(), s.size());
    auto hash2 = hasher.finish();
    

    Die Anwendung ist relativ straight-forward, hoffe ich zumindest. 🙂
    Einfach mal im Header auf das gucken, was nicht unter detail steht. Wobei ich vielleicht schon mal das Interface als Kommentar dazu schreiben könnte. Mache ich bei Gelegenheit. Aber wenn man sich den Rest des Headers anguckt merkt man, dass es im Prinzip nur update und finish in verschiedenen Geschmacksrichtungen gibt.

    Edit: Done.



  • // Use these if you're unsure. They copy the data, but take care of
    // alignment and strict aliasing.
    typedef detail::basic_hasher<word32, 224> sha224_hasher;
    typedef detail::basic_hasher<word32, 256> sha256_hasher;
    typedef detail::basic_hasher<word64, 384> sha384_hasher;
    typedef detail::basic_hasher<word64, 512> sha512_hasher;
    typedef detail::basic_hasher<word64, 224> sha512_224_hasher;
    typedef detail::basic_hasher<word64, 256> sha512_256_hasher;
    

    Der Kommentar macht mich nicht gerade weniger unsure. Was ist mit dem alignment und was hat das mit aliasing zu tun?

    // These may be faster.
    typedef detail::basic_raw_hasher<word32, 224> sha224_raw_hasher;
    typedef detail::basic_raw_hasher<word32, 256> sha256_raw_hasher;
    typedef detail::basic_raw_hasher<word64, 384> sha384_raw_hasher;
    typedef detail::basic_raw_hasher<word64, 512> sha512_raw_hasher;
    typedef detail::basic_raw_hasher<word64, 224> sha512_224_raw_hasher;
    typedef detail::basic_raw_hasher<word64, 256> sha512_256_raw_hasher;
    

    Warum und wie viel schneller?

    Wie sollte ich vorgehen, wenn ich die Daten nur als std::istream vorliegen habe?
    Warum kann man das aktuelle Digest nicht direkt abfragen ohne den Hasher zurückzusetzen?
    Warum sollte man ein Digest nur teilweise abfragen sollen? Das kommt mir unnötig fehleranfällig vor.
    Warum sind die Parameter so nichtssagend benannt? void finish(void* buf, size_t buf_size, const void* data = nullptr, size_t size = 0)
    Warum überhaupt void - und nicht direkt char -Zeiger? Es ergibt doch keinen Sinn, beliebige Objekte zu hashen. Sinnvolle Eingabedaten hat man sowieso immer als char -Folge vorliegen.

    cooky451 schrieb:

    Die Geschwindigkeiten stehen ja als Kommentar im Header:

    * Speeds with VS 2012 on a 3570K @3.4GH:
     * x86: SHA256: ~190 MB/s, SHA512: ~70 MB/s
     * x64: SHA256: ~205 MB/s, SHA512: ~312 MB/s
    

    Total hilfreich, es gibt ja zum Glück nur einen Compiler und einen Prozessor auf der Welt. Was da überhaupt gemessen wurde ist auch irrelevant.

    Ist die Implementation viel langsamer als es theoretisch möglich ist?



  • TyRoXx schrieb:

    Der Kommentar macht mich nicht gerade weniger unsure. Was ist mit dem alignment und was hat das mit aliasing zu tun?

    Der Punkt ist, dass die Daten von dem "high-level" Hasher in ein internes Array kopiert werden. Wenn man sich aber sicher ist, dass die Daten schon so vorliegen dass man als word darauf zugreifen kann, kann man einfach entsprechend casten. Folgt mehr oder weniger aus dem Interface. Etwa 5% langsamer.

    TyRoXx schrieb:

    Wie sollte ich vorgehen, wenn ich die Daten nur als std::istream vorliegen habe?

    Die Daten (nacheinander) auslesen und immer über update() einspeisen.

    TyRoXx schrieb:

    Warum kann man das aktuelle Digest nicht direkt abfragen ohne den Hasher zurückzusetzen?

    So einfach ist das nun nicht. Man bekommt bei finish() nicht einfach nur den aktuellen Digest vorgesetzt. (Dann hätte ich die Funktion digest() genannt.) Da wird uU noch gepadded etc. (Und nach finish() wird der Hasher auch resettet.)

    TyRoXx schrieb:

    Warum sollte man ein Digest nur teilweise abfragen sollen? Das kommt mir unnötig fehleranfällig vor.

    Hmtja, ich dachte es sollte zumindest möglich sein. Für besonders fehleranfällig halte ich es jetzt allerdings nicht, normalerweise wird man ja einfach die Array-Versionen nehmen.
    Edit: Abgesehen davon könnte es sein, dass man die Daten einfach an einer bestimmten Stelle im Speicher haben will und so spart man sich das doppelte Kopieren. Das ist dann sicherlich auch Fehleranfälliger, okay, aber das wäre es auch wenn man hinterher selbst kopiert.

    TyRoXx schrieb:

    Warum sind die Parameter so nichtssagend benannt?

    Hm, wie würdest du sie denn nennen?

    TyRoXx schrieb:

    Warum überhaupt void - und nicht direkt char -Zeiger? Es ergibt doch keinen Sinn, beliebige Objekte zu hashen. Sinnvolle Eingabedaten hat man sowieso immer als char -Folge vorliegen.

    Sehe ich nicht so. const char* schreit eher nach C-String. Hashen kann man aber eigentlich alles, Speicher halt. Ob das jetzt ein String, WString oder irgendwelche Binärdaten sind, spielt eine Rolle.

    TyRoXx schrieb:

    Total hilfreich, es gibt ja zum Glück nur einen Compiler und einen Prozessor auf der Welt. Was da überhaupt gemessen wurde ist auch irrelevant.

    Schon gut, schon gut. Ich lege ein kleines Progrämmchen bei mit dem dann jeder selber messen kann.

    TyRoXx schrieb:

    Ist die Implementation viel langsamer als es theoretisch möglich ist?

    Also die Werte von Crypto++ (das wäre dann wohl handgeschriebenes ASM) sind auf meinem Rechner besser, aber nicht um Größenordnung besser:

    x86: SHA256: ~250MB/s, SHA512: ~250MB/s
    x64: SHA256: ~260MB/s, SHA512: ~330MB/s
    


  • So, bitte sehr. Testprogramm eingebunden und ein paar mehr Kommentare geschrieben. 😉



  • Wieso zwei verschiedene Hasher-Klassen für "alignment passt" und "bin mir nicht sicher"?
    Wieso nicht zwei Funktionen um die Daten durchzupumpen?
    Also quasi der basic_hasher kann ruhig auch die update -Overloads für block_type haben.

    Oder noch besser: check in update(void*, size_t) ob das Alignment passt, und ruf dann die passende Funktion auf (kopierend bzw. nicht-kopierend). Wobei ich mir nicht sicher bin wie bzw. ob überhaupt man das ohne strict-aliasing Verletzung hinbekommt.



  • hustbaer schrieb:

    Wieso zwei verschiedene Hasher-Klassen für "alignment passt" und "bin mir nicht sicher"?

    So klein ist der Unterschied nun auch nicht. Beim raw Hasher muss man ja die Daten auch in passenden Blöcken da haben.

    hustbaer schrieb:

    Wieso nicht zwei Funktionen um die Daten durchzupumpen?
    Also quasi der basic_hasher kann ruhig auch die update -Overloads für block_type haben.

    Einfach die gleiche Funktion haben geht nicht, weil ja etwas im "Puffer" stehen könnte, das dann zuerst dran kommen muss. Sicher könnte man es hinfrickeln, aber dann ist basic_hasher wirklich nicht mehr Idiotensicher. 🙂

    hustbaer schrieb:

    Oder noch besser: check in update(void*, size_t) ob das Alignment passt, und ruf dann die passende Funktion auf (kopierend bzw. nicht-kopierend). Wobei ich mir nicht sicher bin wie bzw. ob überhaupt man das ohne strict-aliasing Verletzung hinbekommt.

    Erstmal gibt das wieder das Problem von oben. Heißt, wenn used_ != 0 kann man das vergessen. Okay, ich könnte natürlich beides prüfen, bleibt noch strict aliasing. Das ist mir leider ein Dorn im Auge, da niemand auf der Welt so wirklich verstanden zu haben scheint was es bedeutet, und ich mittlerweile 5 verschiedene Interpretationen von Sektion 3.10/10 habe. Wenn ich mich einfach nur danach richten würde was Compiler schaffen wäre es leicht, GCC ist da relativ schlecht und VS interessiert es gar nicht, aus Prinzip. Ich weiß aber auch nicht ob es so viel bringen würde. Wenn man diese paar MB/s wirklich haben will, ist das Nutzen eines raw_hashers sicherlich nicht besonders schwer - und der verbraucht dann auch gleich noch weniger Speicher, weil er eben keinen Puffer braucht. Auch wenn ein einzelner Hasher natürlich schöner wäre, sehe ich da momentan noch nicht so ganz die Möglichkeit.

    Edit: Ich habe mal einen neuen (und hoffentlich besseren) Benchmark geschrieben* der die Hasher vergleicht. Der Unterschied wird jetzt noch kleiner, so gegen 1%. Ich denke somit sollte auch klar sein, dass ich an der Stelle nicht mehr viel optimieren kann. Der Vorteil des raw_hashers ist denke ich eher der Speicherverbrauch, falls man auf so etwas wirklich angewiesen sein sollte. Aber grundsätzlich kann man wohl problemlos immer den normalen Hasher nehmen.

    * Und hochgeladen natürlich.



  • Gefällt mir gut, danke für das Teilen. 🙂
    Könnte mir gut vorstellen deinen Code in einem Projekt zu verwenden, wenn ich sporadisch ein paar Daten hashen muss und keine Lust habe mir etwas Dickes wie Crypto++ ins Boot zu holen.


Anmelden zum Antworten