【问题标题】:Fastest way to calculate the Hamming distance between 2 binary vectors in Python / Cython / Numpy在 Python / Cython / Numpy 中计算 2 个二进制向量之间的汉明距离的最快方法
【发布时间】:2021-04-18 12:50:13
【问题描述】:
我正在尝试计算二进制向量和二进制向量矩阵之间的汉明距离。我能找到的最快方法是在 Numpy 中使用无符号 8 位整数:
import numpy as np
np.count_nonzero(data[0] != data, axis=1)
但是,这种方法的问题在于它首先找到所有不同的位,然后对差异的数量求和。这不是有点浪费吗?我尝试在 C++ 中实现一个基本版本,在该版本中,我还计算了不同的位数,这样最后不需要求和,但这要慢得多。可能是因为 Numpy 使用 SIMD 指令。
所以我的问题是。有没有办法在 Numpy / Python / Cython 中使用 SIMD 指令直接计算汉明距离?
【问题讨论】:
标签:
python
numpy
cython
simd
hamming-distance
【解决方案1】:
理想情况下,您真正希望 CPU 执行的操作是 sum += popcount( a[i] ^ b[i]) 具有尽可能大的块。例如在 x86 上,使用 AVX2 对一条指令一次异或 32 个字节,然后再使用几条指令(包括 vpshufb 和 vpaddq)将计数累积到每个元素计数的 SIMD 向量中(最后水平求和)。
对于特定 ISA(如 x86-64)的 C++ 内部函数,这很容易。
您可以使用std::bitset<64> 对64 位块进行异或运算,并将.count() 作为可移植API 来实现高效popcount,从而接近可移植代码。 Clang 可以将标量 popcount 自动矢量化到 AVX2 中,但 GCC 不能。
为了在不违反严格别名的情况下安全地构造它,您可能需要将 memcpy 从另一种类型的任意数据转换为 unsigned long long。
我不知道 Numpy 是否为此编译了一个循环,否则您可能需要在一次执行中进行异或,然后在另一次执行中执行 popcount,这会降低计算强度,因此您肯定希望缓存阻止它在您重新读取它们之前,将它们分成在 L1d 缓存中保持热的小块。
【讨论】:
-
-
Clang has a bit of a missed optimization 它似乎是每次迭代的水平总和。另外我猜想如果你使用AVX2(没有vpopcnt指令)你会想要最小的位宽s.t,在和向量中没有累加器会溢出,所以手动popcnt会尽可能少的步骤? Clang 似乎在做一些完全独立的事情,但还没有测试过。
-
@Noah:__builtin_popcountll() 直接在 uint64_t 数组上没有问题。 godbolt.org/z/K95hh9 要么是位集问题,要么是 32 位整数宽度。我建议报告这个错过优化的错误。
-
有点奇怪。因此,如果 sum 类型的宽度小于 popcnt 的宽度,那么它将在每次迭代时减小到总和宽度,而不是在最后。即uint32_t 与_builtin_popcnt 相加很好。 uint64_t 与 __builtin_popcntll 相加很好。但是uint32_t 与__builtin_popcntll 的总和会减少。我猜__builtin_popcntll 会返回一个long long,尽管永远无法达到 64 以上。(如果您尝试通过更大的临时值(例如uint64_t tmp = __builtin_popcntll())进行累积,那么它是 u32 sum vectorization fails
-
我真的认为尽可能窄的宽度是最好的,因为它对于手动 popcnt 的步骤更少。但我认为这可能不是 clang 的问题,但实际上是 count 返回错误类型的问题。如果__builtin_popcnt + uint32_t clang 可以。 bitset<32>.count() + uint32_t adds an extra step。如果我不得不猜测 line 198 of source code 使用 __builtin_popcntl 是什么打破了这一点。