3

我正在尝试尽可能优化 merkle 根计算。到目前为止,我在 Python 中实现了它,这导致了这个问题以及用 C++ 重写它的建议。

#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <streambuf>
#include <sstream>

#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/crypto.h>



std::vector<unsigned char> double_sha256(std::vector<unsigned char> a, std::vector<unsigned char> b)
{
    unsigned char inp[64];
    int j=0;
    for (int i=0; i<32; i++)
    {
        inp[j] = a[i];
        j++;
    }
    for (int i=0; i<32; i++)
    {
        inp[j] = b[i];
        j++;
    }

    const EVP_MD *md_algo = EVP_sha256();
    unsigned int md_len = EVP_MD_size(md_algo);
    std::vector<unsigned char> out( md_len );
    EVP_Digest(inp, 64, out.data(), &md_len, md_algo, nullptr);
    EVP_Digest(out.data(), md_len, out.data(), &md_len, md_algo, nullptr);
    return out;
}

std::vector<std::vector<unsigned char> > calculate_merkle_root(std::vector<std::vector<unsigned char> > inp_list)
{
   std::vector<std::vector<unsigned char> > out;
   int len = inp_list.size();
   if (len == 1)
   {
        out.push_back(inp_list[0]);
        return out;
   }
   for (int i=0; i<len-1; i+=2)
   {
        out.push_back(
            double_sha256(inp_list[i], inp_list[i+1])
        );
   }
   if (len % 2 == 1)
   {
        out.push_back(
            double_sha256(inp_list[len-1], inp_list[len-1])
        );
   }
   return calculate_merkle_root(out);
}



int main()
{
    std::ifstream infile("txids.txt");

    std::vector<std::vector<unsigned char> > txids;
    std::string line;
    int count = 0;
    while (std::getline(infile, line))
    {
        unsigned char* buf = OPENSSL_hexstr2buf(line.c_str(), nullptr);
        std::vector<unsigned char> buf2;
        for (int i=31; i>=0; i--)
        {
            buf2.push_back(
                buf[i]
            );
        }
        txids.push_back(
            buf2
        );
        count++;
    }
    infile.close();
    std::cout << count << std::endl;

    std::vector<std::vector<unsigned char> > merkle_root_hash;
    for (int k=0; k<1000; k++)
    {
        merkle_root_hash = calculate_merkle_root(txids);
    }
    std::vector<unsigned char> out0 = merkle_root_hash[0];
    std::vector<unsigned char> out;
    for (int i=31; i>=0; i--)
    {
        out.push_back(
            out0[i]
        );
    }

    static const char alpha[] = "0123456789abcdef";
    for (int i=0; i<32; i++)
    {
        unsigned char c = out[i];
        std::cout << alpha[ (c >> 4) & 0xF];
        std::cout << alpha[ c & 0xF];
    }
    std::cout.put('\n');

    return 0;
}

但是,与 Python 实现(~4s)相比,性能更差:

$ g++ test.cpp -L/usr/local/opt/openssl/lib -I/usr/local/opt/openssl/include -lcrypto
$ time ./a.out 
1452
289792577c66cd75f5b1f961e50bd8ce6f36adfc4c087dc1584f573df49bd32e

real      0m9.245s
user      0m9.235s
sys       0m0.008s

完整的实现和输入文件可在此处获得:test.cpptxids.txt

我怎样才能提高性能?默认情况下是否启用编译器优化?是否有比openssl可用的更快的 sha256 库?

4

2 回答 2

3

我决定从头开始实现 Merkle Root 和 SHA-256 计算,使用以SSE2AVX2AVX512闻名的 SIMD(单指令多数据)方法实现了完整的 SHA-256 。

我下面的 AVX2 案例代码的3.5x速度比 OpenSSL 版本7.3x快几倍,比 Python 的hashlib实现快几倍。

这里我提供了 C++ 实现,我也以同样的速度做了 Python 实现(因为它在核心中使用了 C++ 代码),对于 Python 实现参见相关帖子。Python 实现绝对比 C++ 更容易使用。

我的代码非常复杂,既因为它具有完整的 SHA-256 实现,也因为它有一个用于抽象任何 SIMD 操作的类,还有许多测试。

首先,我提供了在 Google Colab上制作的时间,因为那里有非常先进的 AVX2 处理器:

MerkleRoot-Ossl 1274 ms
MerkleRoot-Simd-GEN-1 1613 ms
MerkleRoot-Simd-GEN-2 1795 ms
MerkleRoot-Simd-GEN-4 788 ms
MerkleRoot-Simd-GEN-8 423 ms
MerkleRoot-Simd-SSE2-1 647 ms
MerkleRoot-Simd-SSE2-2 626 ms
MerkleRoot-Simd-SSE2-4 690 ms
MerkleRoot-Simd-AVX2-1 407 ms
MerkleRoot-Simd-AVX2-2 403 ms
MerkleRoot-Simd-AVX2-4 489 ms

Ossl用于测试 OpenSSL 实现,其余的是我的实现。AVX512在速度上的提升更大,这里不做测试,因为Colab不支持AVX512。速度的实际改进取决于处理器能力。

使用以下命令在 Windows (MSVC) 和 Linux (CLang) 中测试编译:

  1. 支持 OpenSSL 的 Windowscl.exe /O2 /GL /Z7 /EHs /std:c++latest sha256_simd.cpp -DSHS_HAS_AVX2=1 -DSHS_HAS_OPENSSL=1 /MD -Id:/bin/OpenSSL/include/ /link /LIBPATH:d:/bin/OpenSSL/lib/ libcrypto_static.lib libssl_static.lib Advapi32.lib User32.lib Ws2_32.lib为您的目录提供已安装的 OpenSSL。如果不需要 OpenSSL 支持,请使用cl.exe /O2 /GL /Z7 /EHs /std:c++latest sha256_simd.cpp -DSHS_HAS_AVX2=1. 在这里也可以代替AVX2你使用SSE2or AVX512。Windows openssl 可以从这里下载。

  2. clang++-12 -march=native -g -m64 -O3 -std=c++20 sha256_simd.cpp -o sha256_simd.exe -DSHS_HAS_OPENSSL=1 -lssl -lcrypto如果需要 OpenSSL,如果不需要,则通过 Linux CLang 编译完成clang++-12 -march=native -g -m64 -O3 -std=c++20 sha256_simd.cpp -o sha256_simd.exe。如您所见,使用了最新的 clang-12 来安装它bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"(此命令在此处进行了描述)。Linux 版本自动检测当前 CPU 架构并使用最佳 SIMD 指令集。

我的代码需要C++20标准支持,因为它使用了一些高级功能来更轻松地实现所有功能。

我在我的库中实现了 OpenSSL 支持,只是为了比较时间以显示我的 AVX2 版本3-3.5x快了几倍。

还提供在 GodBolt 上完成的计时,但这些只是 AVX-512 使用的示例,因为 GodBolt CPU 具有先进的 AVX-512。不要使用 GodBolt 来实际测量时间,因为那里的所有时间都会上下跳跃 5 倍,似乎是因为操作系统驱逐了活动进程。还为操场提供GodBolt 链接(此链接可能有一些过时的代码,请使用我帖子底部的最新代码链接):

MerkleRoot-Ossl 2305 ms
MerkleRoot-Simd-GEN-1 2982 ms
MerkleRoot-Simd-GEN-2 3078 ms
MerkleRoot-Simd-GEN-4 1157 ms
MerkleRoot-Simd-GEN-8 781 ms
MerkleRoot-Simd-GEN-16 349 ms
MerkleRoot-Simd-SSE2-1 387 ms
MerkleRoot-Simd-SSE2-2 769 ms
MerkleRoot-Simd-SSE2-4 940 ms
MerkleRoot-Simd-AVX2-1 251 ms
MerkleRoot-Simd-AVX2-2 253 ms
MerkleRoot-Simd-AVX2-4 777 ms
MerkleRoot-Simd-AVX512-1 257 ms
MerkleRoot-Simd-AVX512-2 741 ms
MerkleRoot-Simd-AVX512-4 961 ms

我的代码使用示例可以在Test()测试我的库的所有功能的函数中看到。我的代码有点脏,因为我不想花太多时间创建漂亮的库,而只是为了证明基于SIMD的实现可以比 OpenSSL 版本快得多。

如果您真的想使用我的基于 SIMD 的增强版本而不是 OpenSSL,并且如果您非常关心速度,并且您对如何使用它有疑问,请在评论或聊天中询问我。

此外,我没有为实现多核/多线程版本而烦恼,我认为如何做到这一点很明显,你可以而且应该毫无困难地实现它。

提供下面代码的外部链接,因为我的代码51 KB大小超过30 KB了 StackOverflow 帖子所允许的文本大小。

sha256_simd.cpp

于 2021-05-08T17:46:12.927 回答
2

您可以做很多事情来优化代码。

以下是要点列表:

  • 需要启用编译器优化-O3(在 GCC 中使用);
  • std::array可以用来代替较慢的动态大小std::vector(因为散列的大小是 32),Hash为了清晰起见,甚至可以定义一种新类型;
  • 参数应该通过引用传递(C++默认通过复制传递参数)
  • 可以保留C++向量以预先分配内存空间并避免不需要的副本;
  • OPENSSL_free必须调用释放分配的OPENSSL_hexstr2buf内存;
  • push_back当大小是编译时已知的常数时,应避免使用;
  • 使用std::copy通常比手动复制更快(更清洁);
  • std::reverse通常比手动循环更快(更清洁);
  • 散列的大小应该是 32,但是可以检查使用断言以确保它没问题;
  • count不需要,因为它是txids向量的大小;

这是生成的代码:

#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <streambuf>
#include <sstream>
#include <cstring>
#include <array>
#include <algorithm>
#include <cassert>

#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/crypto.h>

using Hash = std::array<unsigned char, 32>;

Hash double_sha256(const Hash& a, const Hash& b)
{
    assert(a.size() == 32 && b.size() == 32);

    unsigned char inp[64];
    std::copy(a.begin(), a.end(), inp);
    std::copy(b.begin(), b.end(), inp+32);

    const EVP_MD *md_algo = EVP_sha256();
    assert(EVP_MD_size(md_algo) == 32);

    unsigned int md_len = 32;
    Hash out;
    EVP_Digest(inp, 64, out.data(), &md_len, md_algo, nullptr);
    EVP_Digest(out.data(), md_len, out.data(), &md_len, md_algo, nullptr);
    return out;
}

std::vector<Hash> calculate_merkle_root(const std::vector<Hash>& inp_list)
{
   std::vector<Hash> out;
   int len = inp_list.size();
   out.reserve(len/2+2);
   if (len == 1)
   {
        out.push_back(inp_list[0]);
        return out;
   }
   for (int i=0; i<len-1; i+=2)
   {
        out.push_back(double_sha256(inp_list[i], inp_list[i+1]));
   }
   if (len % 2 == 1)
   {
        out.push_back(double_sha256(inp_list[len-1], inp_list[len-1]));
   }
   return calculate_merkle_root(out);
}

int main()
{
    std::ifstream infile("txids.txt");

    std::vector<Hash> txids;
    std::string line;
    while (std::getline(infile, line))
    {
        unsigned char* buf = OPENSSL_hexstr2buf(line.c_str(), nullptr);
        Hash buf2;
        std::copy(buf, buf+32, buf2.begin());
        std::reverse(buf2.begin(), buf2.end());
        txids.push_back(buf2);
        OPENSSL_free(buf);
    }
    infile.close();
    std::cout << txids.size() << std::endl;

    std::vector<Hash> merkle_root_hash;
    for (int k=0; k<1000; k++)
    {
        merkle_root_hash = calculate_merkle_root(txids);
    }
    Hash out0 = merkle_root_hash[0];
    Hash out = out0;
    std::reverse(out.begin(), out.end());

    static const char alpha[] = "0123456789abcdef";
    for (int i=0; i<32; i++)
    {
        unsigned char c = out[i];
        std::cout << alpha[ (c >> 4) & 0xF];
        std::cout << alpha[ c & 0xF];
    }
    std::cout.put('\n');

    return 0;
}

在我的机器上,这段代码比初始版本快 3 倍,比 Python 实现快 2 倍。

此实现花费 >98% 的时间在EVP_Digest. 因此,如果您想要更快的代码,您可以尝试找到更快的哈希库,尽管 OpenSSL 应该已经非常快了。当前代码已经成功地在主流 CPU 上每秒连续计算 170 万次哈希。这是相当不错的。或者,您也可以使用OpenMP并行化程序(这在我的 6 核机器上大约快 5 倍)。

于 2021-05-03T08:47:48.657 回答