【问题标题】:Very fast rolling hash in Python?Python中非常快速的滚动哈希?
【发布时间】:2020-05-20 08:33:45
【问题描述】:

我正在用 Python 编写一个类似 rsync 的玩具工具。像许多类似的工具一样,它会首先使用一个非常快的哈希作为rolling hash,然后在找到匹配项后使用 SHA256(但后者不在此处:SHA256、MDA5 等太慢了)滚动哈希)。

我目前正在测试各种快速哈希方法:

import os, random, time

block_size = 1024  # 1 KB blocks
total_size = 10*1024*1024  # 10 MB random bytes
s = os.urandom(total_size)

t0 = time.time()
for i in range(len(s)-block_size):
    h = hash(s[i:i+block_size])
print('rolling hashes computed in %.1f sec (%.1f MB/s)' % (time.time()-t0, total_size/1024/1024/(time.time()-t0)))

我得到:0.8 MB/s ... 所以 Python 内置的 hash(...) 函数在这里太慢了。

哪种解决方案可以在标准机器上实现至少 10 MB/s 的更快哈希?

  • 我试过了

    import zlib
    ...
        h = zlib.adler32(s[i:i+block_size])
    

    但也好不了多少(1.1 MB/s)

  • 我试过sum(s[i:i+block_size]) % modulo,它也很慢

  • 有趣的事实:即使没有任何哈希函数,循环本身也很慢!

    t0 = time.time()
    for i in range(len(s)-block_size):
        s[i:i+block_size]
    

    我得到:只有 3.0 MB/s!因此,在s 上循环访问滚动块的简单事实已经很慢了。

与其重新发明轮子并编写我自己的哈希/或使用自定义 Rabin-Karp 算法,您有什么建议,首先加快这个循环,然后作为哈希?


编辑:上面“有趣的事实”慢循环的(部分)解决方案:

import os, random, time, zlib
from numba import jit

@jit()
def main(s):
    for i in range(len(s)-block_size):
        block = s[i:i+block_size]

total_size = 10*1024*1024  # 10 MB random bytes
block_size = 1024  # 1 KB blocks
s = os.urandom(total_size)
t0 = time.time()
main(s)
print('rolling hashes computed in %.1f sec (%.1f MB/s)' % (time.time()-t0, total_size/1024/1024/(time.time()-t0)))

使用 Numba,有很大的改进:40.0 MB/s,但这里仍然没有进行哈希处理。至少我们不会以 3 MB/s 的速度被阻止。

【问题讨论】:

  • 每次重新计算整个块的哈希不是“滚动哈希”。您计算一个完整的哈希一次,然后对于每个步骤,您只使用两个字节的数据来更新该计算 - 一个在开始时刚刚退出块,一个在开始时刚刚进入块结尾。这与大多数哈希函数不兼容,但如果您使用所有字节的总和或 XOR,这将是微不足道的。
  • @jasonharper 即使有一个带有滑动窗口的循环并且没有散列,它已经很慢了(2.4MB/s)。我找到的唯一方法是 Numba (请参阅最后的更新问题)。
  • 您的循环仍然在每一步制作一个块大小的数据切片 - 这是无缘无故地复制大量数据。
  • @jasonharper 我认为block = s[i:i+block_size] 没有复制,它只是对该块的引用/视图,对吗?
  • 我认为任何内置的 Python 类型在切片时都不会在现有对象中创建视图(这是一个有点问题的方法 - 由于保留原始切片的小切片太容易遇到内存问题活着的物体)。你必须使用 numpy 来获得这种行为。

标签: python for-loop hash sha256 rolling-computation


【解决方案1】:

而不是重新发明轮子并编写我自己的哈希/或使用自定义 Rabin-Karp 算法,你有什么建议,首先加快这个速度 循环,然后作为哈希?

从这种心态开始总是很好,但似乎你没有得到滚动哈希的想法。 使散列函数非常适合滚动的原因在于它能够重用先前的处理。

一些散列函数允许非常计算滚动散列 快速——只给旧的哈希值快速计算新的哈希值 哈希值、从窗口中移除的旧值和新值 添加到窗口中。

来自同一个wikipedia page

如果没有timeit,很难比较不同机器的性能,但我将您的脚本更改为使用带素数模数的简单多项式散列(使用Mersene prime 会更快,因为模运算可能是用二元运算完成):

import os, random, time

block_size = 1024  # 1 KB blocks
total_size = 10*1024*1024  # 10 MB random bytes
s = os.urandom(total_size)

base = 256
mod  = int(1e9)+7

def extend(previous_mod, byte):
    return ((previous_mod * base) + ord(byte)) % mod

most_significant = pow(base, block_size-1, mod)

def remove_left(previous_mod, byte):
    return (previous_mod - (most_significant * ord(byte)) % mod) % mod
    
def start_hash(bytes):
    h = 0
    for b in bytes:
        h = extend(h, b)
    return h

t0 = time.time()

h = start_hash(s[:block_size])
for i in range(block_size, len(s)):
    h = remove_left(h, s[i - block_size])
    h = extend(h, s[i])
    
print('rolling hashes computed in %.1f sec (%.1f MB/s)' % (time.time()-t0, total_size/1024/1024/(time.time()-t0)))

显然,您使用 Numba 实现了相当大的改进,它也可以加快此代码的速度。 为了提高性能,您可能需要编写一个 C(或 Rust 等其他低级语言)函数来处理列表的一大片,并返回一个带有哈希值的数组。

我也在创建类似rsync 的工具,但正如我在 Rust 中所写的那样,这个级别的性能不是我关心的问题。相反,我遵循creator of rsync 的提示并尝试并行化所有我能做的事情,这是在 Python 中完成的一项痛苦的任务(如果没有 Jython 可能是不可能的)。

【讨论】:

    【解决方案2】:

    你有什么建议,首先加快这个循环,然后作为一个哈希?

    增加块大小。块大小越小,每字节执行的 python 越多,速度也会越慢。

    编辑:您的范围的默认步长为 1,并且您不会将 i 乘以 block_size,因此不是在 10*1024 个不重叠的 1k 块上进行迭代,而是在 1000 万个上进行迭代 - 1024个大部分重叠的块

    【讨论】:

    • 没有这么简单(你可以尝试用更高的block_size值运行我的代码)。
    • 确实如此,而且我想我刚刚意识到问题所在:您没有按块大小进行步进,因此您正在对每个字节的块进行哈希处理。不是散列 10k 块,而是散列 1m(大部分重叠)块。
    • 正是这个@Maslkkinn!在分析文件old.rawnew.raw 之间的变化时,总是有可能在文件中间插入了一个字节,因此必须计算滚动哈希,步骤确实为1。跨度>
    【解决方案3】:

    首先,您的慢循环。如前所述,您正在为流中的每个字节(较小的块大小)切片一个新块。这在 cpu 和内存上都有很多工作。

    更快的循环是将数据预先分块成并行位。

    chunksize = 4096 # suggestion
    # roll the window over the previous chunk's last block into the new chunk
    lastblock = None
    for readchunk in read_file_chunks(chunksize):
        for i in range(0, len(readchunk), blocksize):
            # slice a block only once
            newblock = readchunk[i:blocksize]
            if lastblock:
                for bi in range(len(newblock)):
                    outbyte = lastblock[bi]
                    inbyte = newblock[bi]     
                    # update rolling hash with inbyte and outbyte
                    # check rolling hash for "hit"
            else:
                pass # calculate initial weak hash, check for "hit"
            lastblock = newblock
    

    块大小应该是块大小的倍数

    接下来,您将依次计算整个每个块的“滚动哈希”,而不是以“滚动”方式逐字节更新哈希。那是非常慢的。上面的循环迫使您在字节进出窗口时处理它们。尽管如此,我的试验显示吞吐量非常差(~3Mbps~ 编辑:抱歉,这是 3MiB/s),即使每个字节上的算术运算数量适中。编辑:我最初有一个 zip() 并且看起来相当慢。在没有 zip 的情况下,仅循环的整个循环就增加了一倍以上(上面的当前代码)

    Python 是单线程和解释的。我看到一个 cpu 被固定,这就是瓶颈。为了更快,您需要多个线程(子进程)或闯入 C,或两者兼而有之。我认为简单地在 C 中运行数学可能就足够了。 (哈哈,“简单”)

    【讨论】:

    • 看来python的迭代根本不能比这快。我编写了一个小型 c 程序来读取文件并为每个输入字节(减去第一个块)输出一个 4 字节的“fastsum”。仅此一项就以超过 1.2Gbps 的速度运行(从 nvme 读取)。如果我将该输出传递到一个 python 脚本中,该脚本评估每个 4 字节的总和(不执行任何操作,只是进行一次迭代),这会将文件输入速度降低到 20Mbps 以下。 :(
    猜你喜欢
    • 2015-08-30
    • 2016-04-11
    • 2015-01-11
    • 2013-09-12
    • 2013-04-19
    • 2014-03-26
    • 1970-01-01
    • 2016-10-27
    • 2013-12-23
    相关资源
    最近更新 更多