c++boostopensslcrypto++

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.


Solution

  • 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:


        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.