问题描述
我在 Python 中的实现计算了大约 1500 个输入哈希的默克尔根哈希:
import numpy as np
from binascii import unhexlify,hexlify
from hashlib import sha256
txids = np.loadtxt("txids.txt",dtype=str)
def double_sha256(a,b):
inp = unhexlify(a)[::-1] + unhexlify(b)[::-1]
sha1 = sha256(inp).digest()
sha2 = sha256(sha1).digest()
return hexlify(sha2[::-1])
def calculate_merkle_root(inp_list):
if len(inp_list) == 1:
return inp_list[0]
out_list = []
for i in range(0,len(inp_list)-1,2):
out_list.append(double_sha256(inp_list[i],inp_list[i+1]))
if len(inp_list) % 2 == 1:
out_list.append(double_sha256(inp_list[-1],inp_list[-1]))
return calculate_merkle_root(out_list)
for i in range(1000):
merkle_root_hash = calculate_merkle_root(txids)
print(merkle_root_hash)
由于 merkle root 计算了 1000 次,所以一次计算大约需要 5ms:
$ time python3 test.py
b'289792577c66cd75f5b1f961e50bd8ce6f36adfc4c087dc1584f573df49bd32e'
real 0m5.132s
user 0m5.501s
sys 0m0.133s
如何提高计算速度?这段代码可以优化吗?
到目前为止,我已经尝试在 Python 和 C++ 中展开递归函数。然而,性能并没有提高,只花了~6ms。
编辑
编辑 2
由于评论中的建议,我删除了 unhexlify
和 hexlify
不必要的步骤。在循环之前,列表准备好一次。
def double_sha256(a,b):
inp = a + b
sha1 = sha256(inp).digest()
sha2 = sha256(sha1).digest()
return sha2
def map_func(t):
return unhexlify(t)[::-1]
txids = list(map(map_func,txids))
for i in range(1000):
merkle_root_hash = calculate_merkle_root(txids)
merkle_root_hash = hexlify(merkle_root_hash[::-1])
现在执行是~4ms:
$ time python3 test2.py
b'289792577c66cd75f5b1f961e50bd8ce6f36adfc4c087dc1584f573df49bd32e'
real 0m3.697s
user 0m4.069s
sys 0m0.128s
解决方法
我决定完全从头开始实施 SHA-256,并使用 SIMD 指令集(在此处阅读它们SSE2、AVX2、AVX512)。
因此,我的以下 AVX2 案例代码的速度比 OpenSSL 版本快 3.5x
倍,比 Python 的 7.3x
实现快 hashlib
倍。
我还创建了有关 C++ 版本的第二篇文章,see it here。阅读 C++ 帖子以了解有关我的库的更多详细信息,这篇 Python 帖子更高级。
首先提供时间:
simple 3.006
openssl 1.426
simd gen 1 1.639
simd gen 2 1.903
simd gen 4 0.847
simd gen 8 0.457
simd sse2 1 0.729
simd sse2 2 0.703
simd sse2 4 0.718
simd sse2 8 0.776
simd avx2 1 0.461
simd avx2 2 0.41
simd avx2 4 0.549
simd avx2 8 0.521
这里的 simple
是 hashlib 的版本,与您提供的版本接近,openssl
代表 OpenSSL 版本,其余 simd
版本是我的 SIMD (SSE2/AVX2/AVX512) 实现。如您所见,AVX2 版本比 3.5x
版本快 OpenSSL
倍,比原生 Python 的 7.3x
快 hashlib
倍。
上面的计时是在 Google Colab 中完成的,因为它们有非常先进的 AVX2 CPU。
在底部提供库的代码,因为代码非常庞大,所以它作为单独的链接发布,因为它不符合 StackOverflow 的 30 KB
限制。有两个文件 sha256_simd.py
和 sha256_simd.hpp
。 Python 的文件包含计时和使用示例,还包含基于 Cython 的包装器以使用我在 .hpp 文件中提供的 C++ 库。这个python文件包含编译和运行代码所需的一切,只需将这两个文件放在附近并运行python文件即可。
我在 Windows(MSVC 编译器)和 Linux(CLang 编译器)上测试了这个程序/库。
我的库的使用示例位于 merkle_root_simd_example()
和 main()
函数中。基本上你会做以下事情:
-
首先通过
mod = sha256_simd_import(cap = 'avx2')
导入我的库,每次程序运行只执行一次,不要多次执行,记住这个返回的模块到某个全局变量中。在cap
参数中,您应该输入 CPU 支持的任何内容,它可以是gen
或sse2
或avx2
或avx512
,以增加技术复杂性和提高速度.gen
是通用非 SIMD 操作,sse2
是 128 位操作,avx2
是 256 位操作,avx512
是 512 位操作。 -
导入后使用导入的模块,例如
mod.merkle_root_simd('avx2',2,txs)
。在这里,您再次放置了gen
/sse2
/avx2
/avx512
技术之一。为什么又来了?第一次导入时放置编译选项,该选项告诉编译器支持给定和以下所有技术。这里放了将用于 merkle-root 调用的 SIMD 技术,该技术可以低于(但不能高于)编译技术。例如,如果您为avx2
编译,那么您可以将库用于gen
或sse2
或avx2
,但不能用于avx512
。 -
你可以在 2) 中看到我使用了选项
('avx2',txs)
,这里的2
表示并行化参数,它不是多核而是单核并行化,这意味着两个 avx2 寄存器将是连续计算。您应该输入 1 或 2 或 4 或 8,无论哪个为您提供更快的计算。
为了使用库,您必须安装两件事 - 一是编译器(Windows 的 MSVC 和 Linux 的 CLang(或 GCC)),第二 - 通过 python -m pip install cython
安装一次 Cython 模块,Cython是一个用于在 Python 中编程 C++ 代码的高级库,在这里它充当我的 Python 的 .py
和 C++ 的 .hpp
模块之间的薄包装器。此外,我的代码是使用最现代的 C++20 标准编写的,请注意这一点,您必须拥有最新的 C++ 编译器才能编译我的代码,以便在 Windows 上下载最新的 MSVC 和/或最新的用于 Linux 的 CLang(通过命令 bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
,它被描述为 here)。
在 .py 文件中,您可以看到我有时会提供额外的参数 has_ossl = True,win_ossl_dir = 'd:/bin/openssl/'
,仅当您需要将 OpenSSL 版本编译到我的库中时才需要这两个参数。 Windows openssl 可以从 here 下载。以后的 openssl 版本可以通过 mod.merkle_root_ossl(txs)
使用,只需为事务提供单个参数。
在我的 .py 模块中的所有函数版本中,您需要为交易提供字节列表,这意味着如果您有十六进制交易,那么您必须先对它们进行 unhexlify。此外,所有函数都返回字节哈希,这意味着如果需要,您必须对其进行十六进制化。这种仅限字节的往返传输仅出于性能原因。
我了解我的代码非常难以理解和使用。因此,如果您非常希望拥有最快的代码,那么如果您愿意,请向我询问有关如何使用和理解我的代码的问题。另外我应该说我的代码很脏,我并不是想为所有人制作一个干净闪亮的库,我只是想制作一个概念证明,SIMD 版本比 hashlib 的版本快得多,甚至比 openssl版本,因为只有当您的 CPU 非常先进以支持至少 SSE2/AVX2/AVX512 之一时,大多数 CPU 支持 SSE2,但并非所有 CPU 甚至支持 AVX2 和 AVX512。
,在上次更新(2021 年 5 月 2 日 17:00)中,对 sha256(value).digest()
的调用在我的机器上占用了大约 80% 的时间。解决这个问题的可能解决方案很少。
第一个是使用 multiprocessing
并行化计算,假设每次迭代的工作都是独立的。下面是一个例子:
from multiprocessing.pool import Pool
# [...] same as in the question
def iteration(txids):
merkle_root_hash = calculate_merkle_root(txids)
merkle_root_hash = hexlify(merkle_root_hash[::-1])
return merkle_root_hash
processPool = Pool()
res = processPool.map(iteration,[txids for i in range(1000)])
print(res[-1])
这在我的 6 核机器上快了 4 倍。
另一个解决方案是找到一个更快的 Python 模块,它可以同时计算多个 sha256 哈希值,以减少来自 CPython 解释器的昂贵的 C 调用。我不知道有任何包这样做。
最后,一种有效的解决方案是(至少部分地)用 C 或 C++ 重写昂贵的 calculate_merkle_root
计算并并行运行它。这应该比您当前的代码快得多,因为这消除了函数调用开销和多处理成本。有许多库可以计算 sha256 哈希值(例如 Crypto++ 库)。