如何解决C++中如何提高merkle root的计算速度?
我正在尝试尽可能地优化默克尔根计算。到目前为止,我在 Python 中实现了它,结果是 this question 并建议用 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,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.cpp 和 txids.txt。
如何提高性能?默认情况下是否启用编译器优化?是否有比 openssl
更快的 sha256 库?
解决方法
您可以做很多事情来优化代码。
以下是重点列表:
-
编译器优化需要启用(在 GCC 中使用
-O3
); -
std::array
可以用来代替较慢的动态大小std::vector
(因为哈希的大小是 32),甚至可以定义一个新的 {{1 }} 输入清楚; - 参数应该通过引用传递(C++默认通过复制传递参数)
- 可以保留 C++ 向量以预先分配内存空间并避免不需要的副本; 必须调用
-
Hash
来释放OPENSSL_free
的分配内存; -
OPENSSL_hexstr2buf
当大小是编译时已知的常量时应避免使用; - 使用
push_back
通常比手动复制更快(更干净); -
std::copy
通常比手动循环更快(更干净); - 散列的大小应该是 32,但可以使用断言来检查它是否正确;
-
std::reverse
不是必需的,因为它是count
向量的大小;
这是结果代码:
txids
在我的机器上,此代码比初始版本快 3 倍,比 Python 实现快 2 倍。
此实现在 #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,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;
}
上花费了 >98% 的时间。因此,如果您想要更快的代码,您可以尝试找到一个更快的散列库,尽管 OpenSSL 应该已经相当快了。当前代码已经成功地在主流 CPU 上每秒连续计算 170 万个哈希。这很好。或者,您也可以使用 OpenMP 并行化程序(这在我的 6 核机器上大约快 5 倍)。
我决定从头开始实现 Merkle Root 和 SHA-256 计算,实现了完整的 SHA-256,使用 SIMD(单指令多数据)方法,以 SSE2、AVX2、{{3 }}。
我的以下 AVX2 案例代码的速度比 OpenSSL 版本快 3.5x
倍,比 Python 的 7.3x
实现快 hashlib
倍。
这里我提供了 C++ 实现,我也以相同的速度制作了 Python 实现(因为它在核心中使用 C++ 代码),Python 实现见 AVX512。 Python 实现肯定比 C++ 更容易使用。
我的代码非常复杂,因为它具有完整的 SHA-256 实现,还因为它有一个用于抽象任何 SIMD 操作和许多测试的类。
首先,我提供了在 Google related post 上制作的计时,因为那里有非常先进的 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) 中测试编译:
-
支持 OpenSSL 的 Windows
cl.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
或SSE2
代替AVX512
。 Windows openssl 可以从 Colab 下载。 -
Linux CLang 编译通过
clang++-12 -march=native -g -m64 -O3 -std=c++20 sha256_simd.cpp -o sha256_simd.exe -DSHS_HAS_OPENSSL=1 -lssl -lcrypto
完成,如果需要 OpenSSL,如果不需要,则通过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)"
(此命令描述为 here)。 Linux 版本会自动检测当前 CPU 架构并使用最佳 SIMD 指令集。
我的代码需要 C++20
标准支持,因为它使用了一些高级功能来更轻松地实现所有功能。
我在我的库中实现了 OpenSSL 支持只是为了比较时间以表明我的 AVX2 版本快了 3-3.5x
倍。
还提供了在 GodBolt 上完成的计时,但这些只是 AVX-512 使用的示例,因为 GodBolt CPU 具有先进的 AVX-512。不要使用 GodBolt 来实际测量时间,因为那里的所有时间都会上下跳跃 5 倍,这似乎是因为操作系统驱逐了活动进程。还为 Playground 提供了 here(此链接可能有一些过时的代码,请使用我帖子底部的最新代码链接):
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()
函数中看到我的代码使用示例。我的代码有点脏,因为我不想花太多时间创建漂亮的库,而只是想证明基于 GodBolt link 的实现可以比 OpenSSL 版本快得多。
如果您真的想使用我的基于 SIMD 的增强版本而不是 OpenSSL,并且您非常关心速度,并且您对如何使用它有疑问,请在评论或聊天中问我。
此外,我也没有为实现多核/多线程版本而烦恼,我认为如何做到这一点很明显,你可以而且应该毫无困难地实现它。
提供指向以下代码的外部链接,因为我的代码大小约为 51 KB
,超出了 StackOverflow 帖子允许的 30 KB
文本。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。