Ali
Ali

Reputation: 1043

What is the fastest SHA1 implementation?

I am looking for the fastest SHA1 implementation since I have to compute it millions of times. I tried boost::uuids::detail::sha1 and OpenSSL SHA1 and I find OpenSSL 2.5 times faster than boost. I also checked Crypto++ which is much slower than the other two. Here is what I do to check their performances:

OpenSSL SHA1:

#include "openssl/sha.h"

void sha1_ossl (const unsigned char* data) {
    unsigned char  hash[20];
    for (long i=0; i<100000000; ++i) {
      SHA1(data, 64, hash);
    
      if ((unsigned int)hash[0]==0 && (unsigned int)hash[1]==0 && (unsigned int)hash[2]==0 && (unsigned int)hash[3]==0)
          break;
    }
}

Boost::SHA1:

#include <boost/uuid/detail/sha1.hpp>

void sha1_boost (const unsigned char* data) {
    boost::uuids::detail::sha1 sha1;
    unsigned hash[5];
    for (long i=0; i<100000000; ++i) {
        sha1.process_bytes(data, 64);
        sha1.get_digest(hash);
        sha1.reset();
        if (hash[0]==0) break;
    }
}

CryptoPP::SHA1:

#include <cryptopp/sha.h>
#include <cryptopp/hex.h>

void sha1_cryptoPP (const unsigned char* data) {
    std::string data_s (reinterpret_cast<char const*>(data));
    std::string hash_hex;
    CryptoPP::SHA1 sha1;
    for (long i=0; i<100000000; ++i) {
        CryptoPP::StringSource ss(data_s, true, new CryptoPP::HashFilter(sha1, new CryptoPP::HexEncoder(new CryptoPP::StringSink(hash_hex))));
        if (hash_hex.starts_with("00000000")) break;
    }
}

Then I test these functions with a random data:

int main() {
  const unsigned char data[65] = "tJQVfvcjGMNIvJfowXBjmSRcKtSjCcyQvaAdakfEJtgSNZHnOHCjkzGFwngiLFPm";
  sha1_boost (data);
  sha1_ossl (data);
  sha1_cryptoPP (data);
}

Performance Results

I compiled all the codes with g++ -O3 -std=c++2a to get the following results. I found OpenSSL faster than other implementations and Crypto++ the slowest:

Performance of various SHA1 algorithms

Questions

Any feedback to improve performance is appreciated.

Upvotes: 6

Views: 2737

Answers (1)

sehe
sehe

Reputation: 392979

My last experiments confirmed openssl's was fastest among several (including Crypto++ and some lose single-source C implementations that I forgot which ones)

Re: the code-review type parts of the question:

  • the boost "implementation" is from a detail namespace and should not be relied on.

  • The CryptoPP take might benefit from using the procedural interface instead of dynamically composing the pipeline each time. Specifically, you should not be converting to string to check the first n bytes of the digest. This is likely the dominant part of the runtime due to the repeated allocations.

    Adhering to the procedural interface may also allow you to to use the reset/clear member (quoting from memory)


    if ((unsigned int)hash[0] == 0 && (unsigned int)hash[1] == 0 &&
        (unsigned int)hash[2] == 0 && (unsigned int)hash[3] == 0)
        break;

Should have been a simple

    if (!(hash[0] || hash[1] || hash[2] || hash[3]))
        break;

Or even

    if (!std::any_of(hash+0, hash+4, std::identity{}))
        break;

Improved Bench Code

Incorporating some of the above and some more (mostly around good style, avoiding pointer errors, showing effective iterations and allow dump to inspect digests for accuracy):

Live On Compiler Explorer

#include "openssl/sha.h"
#include <boost/uuid/detail/sha1.hpp>
#include <algorithm>
#include <iostream>
#include <iomanip>
using byte = unsigned char;

#ifndef ONLINE_DEMO
auto constexpr iterations = 100'000'000;
#else
auto constexpr iterations = 10000;
#endif

static void dump(byte const (&a)[20]) {
    for (unsigned b : a) {
        std::cout << std::setw(2) << std::setfill('0') << std::hex << b;
    }
    std::cout << std::dec << std::endl;
}

static void dump(uint32_t const (&a)[5]) {
    for (auto b : a) {
        std::cout << std::setw(8) << std::setfill('0') << std::hex << b;
    }
    std::cout << std::dec << std::endl;
}

long sha1_ossl(std::string_view data) {
    byte hash[20];
    for (long i = 0; i < iterations; ++i) {
        SHA1(reinterpret_cast<byte const*>(data.data()), data.size(), hash);

        //dump(hash);
        if (!std::any_of(hash+0, hash+4, std::identity{}))
            return i;
    }
    return iterations;
}

long sha1_boost(std::string_view data) {
    boost::uuids::detail::sha1 sha1;
    uint32_t hash[5];
    for (long i = 0; i < iterations; ++i) {
        sha1.process_bytes(reinterpret_cast<byte const*>(data.data()), data.size());
        sha1.get_digest(hash);
        sha1.reset();
        //dump(hash);
        if (hash[0] == 0)
            return i;
    }
    return iterations;
}

#ifndef ONLINE_DEMO
#include <cryptopp/hex.h>
#include <cryptopp/sha.h>
long sha1_cryptoPP(std::string_view data) {
    byte digest[20];

    CryptoPP::SHA1 sha1;
    for (long i = 0; i < iterations; ++i) {
        sha1.Restart();
        sha1.Update(reinterpret_cast<byte const*>(data.data()), data.size());
        sha1.Final(digest);

        //dump(digest);
        if (!std::any_of(digest+0, digest+4, std::identity{}))
            return i;
    }
    return iterations;
}
#endif

#include <chrono>
using namespace std::chrono_literals;

int main() {
    static auto now = std::chrono::high_resolution_clock::now;
    constexpr static std::string_view data =
        "tJQVfvcjGMNIvJfowXBjmSRcKtSjCcyQvaAdakfEJtgSNZHnOHCjkzGFwngiLFPm";

    auto timed = [](auto caption, auto f) {
        auto const start = now();
        auto n = f(data);
        std::cout << caption << ": " << n << " in " << (now()-start)/1.0ms << "ms\n";
    };

    timed("sha1_boost", sha1_boost);
    timed("sha1_ossl", sha1_ossl);
#ifndef ONLINE_DEMO
    timed("sha1_cryptoPP", sha1_cryptoPP);
#endif
}

Prints:

sha1_boost: 100000000 in 85660.5ms
sha1_ossl: 100000000 in 24652.6ms
sha1_cryptoPP: 100000000 in 34921.3ms

Or Online (no Crypto++ there):

sha1_boost: 10000 in 8.71938ms
sha1_ossl: 10000 in 2.32025ms

That's a significant improvement, but same winner.

Upvotes: 3

Related Questions