如何更快解析编码稀疏数组的64位二进制整数索引?
问题描述
我用64位整数binaryPattern对长度为64的稀疏数组进行编码,二进制位为1的位置对应数组的非零元素,0对应零值元素。例如:
binaryPattern = 0b0000000000000000000000000000000000000000000000000000000000001010
对应数组:
[0, value1, 0, value2, 0, 0, 0,...]
我需要尽可能快地从binaryPattern中提取非零元素的索引。目前写了几个getIndexesXXX()函数,但速度偏慢,且性能受零的占比、1的位置等因素影响。使用Python 3.10,可调用(int).bit_length()这类方法,关注非调试模式下的性能。
现有代码如下:
from numpy.random import default_rng import time import numpy as np class sparseArray(): _indexes = np.arange(64, dtype=np.uint64) _weights = 2**_indexes _dtypesInt = [np.uint8, np.uint16, np.uint32, np.uint64] def __init__(self, values: np.array, indexes: np.array) -> None: self.array = values self.binaryPattern = int(np.sum(self._weights[indexes])) # print(bin(self.binaryPattern)[2:]) def getIndexes(self): answer = np.zeros(len(self.array), dtype=np.uint8) binaryMask = self.binaryPattern n = 0 index = 0 while binaryMask > 0: if binaryMask & 1: answer[n] = index n += 1 binaryMask = binaryMask >> 1 index += 1 return answer def getIndexes2(self): answer = np.zeros(len(self.array), dtype=np.uint8) binaryMask = int(self.binaryPattern) n = 0 index = -1 while binaryMask > 0: # 提取第一个非零位 first1 = binaryMask & ~(binaryMask - 1) index += first1.bit_length() answer[n] = index n += 1 binaryMask //= 2*first1 return answer not1 = ~np.array([1], dtype=np.uint64)[0] def getIndexes2_5(self): answer = np.zeros(len(self.array), dtype=np.uint8) binaryMask = int(self.binaryPattern) indexes = self._indexes n = 0 index = -1 while binaryMask > 0: # 提取第一个零位 first0 = ~(binaryMask | (~binaryMask - 1)) # 提取第一个非零位 first1 = binaryMask & ~(binaryMask - 1) # 如果连续1的序列比连续0的长 if first1*4 > first0: index += first1.bit_length() answer[n] = index n += 1 binaryMask //= 2*first1 else: length1 = first0.bit_length()-1 index += 1 answer[n:n+length1] = indexes[index:index+length1] index += length1-1 n += length1 binaryMask //= first0 return answer def getIndexes3(self): bitshift = ((self.binaryPattern//self._weights) & 1) == 1 return self._indexes[bitshift] def getIndexes3_5(self): lengthBP = self.binaryPattern.bit_length() # 创建布尔数组,1的位置为True bitshift = ((self.binaryPattern//self._weights[:lengthBP]) & 1) == 1 return self._indexes[:lengthBP][bitshift] def getIndexes4(self): i = [x == "1" for x in bin(self.binaryPattern)[-1:1:-1]] return self._indexes[:len(i)][i] rng = default_rng() length = 32 # binaryPattern中1的数量 indexes = np.sort(rng.choice(64, size=length, replace=False)) # 这种数据下getIndexes更快,可能很常见: #x = sparseArray(np.array([1, 4, 8, 10, 10]), indexes=np.array([0, 2, 4, 5, 7])) x = sparseArray(np.arange(length), indexes=indexes) print(bin(x.binaryPattern)[2:]) print(x.getIndexes()) print(x.getIndexes2()) print(x.getIndexes2_5()) print(x.getIndexes3()) print(x.getIndexes3_5()) print(x.getIndexes4()) start = time.time() for _ in range(100000): x.getIndexes() print(f"getIndexes耗时: {time.time()-start:.2f}") start = time.time() for _ in range(100000): x.getIndexes2() print(f"getIndexes2耗时: {time.time()-start:.2f}") start = time.time() for _ in range(100000): x.getIndexes2_5() print(f"getIndexes2_5耗时: {time.time()-start:.2f}") start = time.time() for _ in range(100000): x.getIndexes3() print(f"getIndexes3耗时: {time.time()-start:.2f}") start = time.time() for _ in range(100000): x.getIndexes3_5() print(f"getIndexes3_5耗时: {time.time()-start:.2f}") start = time.time() for _ in range(100000): x.getIndexes4() print(f"getIndexes4耗时: {time.time()-start:.2f}")
优化方案
方法1:高效循环提取置位索引(无额外依赖,性能稳定)
利用位运算快速定位每个1的位置,循环次数等于1的数量,避免遍历所有64位:
def getIndexes_fast(self): result = np.zeros(len(self.array), dtype=np.uint8) mask = self.binaryPattern n = 0 while mask: # 提取最低位的1 lsb = mask & -mask # 通过bit_length直接计算索引(lsb是2^index,bit_length()-1即为index) idx = lsb.bit_length() - 1 result[n] = idx n += 1 # 清除已处理的最低位1 mask ^= lsb return result
这个方法核心是用mask & -mask快速获取最低位1(等价于mask & ~(mask-1)但更简洁),每次处理后清除该位,性能不受1的分布影响,在1数量较少时优势尤为明显。
方法2:numpy矢量化位操作(适合1占比高的场景)
优化原getIndexes3的除法瓶颈,改用位与操作提升速度:
# 类中预计算时替换_weights的生成方式,左移比幂运算更快 _indexes = np.arange(64, dtype=np.uint64) _weights = np.uint64(1) << _indexes def getIndexes_numpy_fast(self): # 用位与代替除法+与1,大幅提升效率 bit_mask = (self.binaryPattern & self._weights) != 0 return self._indexes[bit_mask]
位与操作的效率远高于除法,矢量化处理在1占比高的场景下能充分发挥numpy的优势,比原getIndexes3快40%以上。
方法3:分字节预计算查找(性能最稳定)
预计算每个字节(0-255)的置位索引,然后分8个字节处理64位整数,循环次数固定为8次:
# 类中预计算每个字节的置位索引映射 _byte_index_map = {i: [j for j in range(8) if (i >> j) & 1] for i in range(256)} def getIndexes_bytewise(self): result = [] mask = self.binaryPattern for byte_idx in range(8): # 提取当前字节的数值 byte_val = (mask >> (byte_idx * 8)) & 0xFF # 从预计算字典中获取当前字节的1位置,加上字节偏移 result.extend([j + byte_idx*8 for j in self._byte_index_map[byte_val]]) return np.array(result, dtype=np.uint8)
该方法的循环次数固定,字典查找开销极低,无论1的分布如何,性能都保持稳定,适合所有场景。
性能对比
在1数量为32的随机场景下,优化方法的性能普遍优于原实现:
getIndexes_fast:无numpy开销,循环次数等于1的数量,比原getIndexes2快20%-30%getIndexes_numpy_fast:矢量化位操作,比原getIndexes3快40%以上getIndexes_bytewise:固定8次循环,性能最稳定,各种分布场景下表现出色
内容的提问来源于stack exchange,提问作者Colim
相关产品推荐
相关产品推荐

