具有换档操作的二进制掩模,无循环

时间:2017-05-22 16:05:42

标签: python math binary logical-operators

我们有一些大的二进制数N(大意味着数百万的数字)。我们还有二进制掩码M,其中1表示我们必须删除数字N中此位置的数字,并将所有较高位移动一个位置。

示例:

N = 100011101110
M = 000010001000
Res   1000110110

是否有可能在没有使用某些逻辑或算术运算的循环的情况下解决此问题?我们可以假设我们可以在Python中访问bignum算法。

感觉它应该是这样的: Res = N - (N xor M) 但它不起作用

UPD :我目前的循环解决方案如下:

def prepare_reduced_arrays(dict_of_N, mask):
    '''
    mask: string '0000011000'
    each element of dict_of_N - big python integer
    '''

    capacity = len(mask)
    answer = dict()
    for el in dict_of_N:
        answer[el] = 0

    new_capacity = 0
    for i in range(capacity - 1, -1, -1):
        if mask[i] == '1':
            continue
        cap2 = (1 << new_capacity)
        pos = (capacity - i - 1)
        for el in dict_of_N:
            current_bit = (dict_of_N[el] >> pos) & 1
            if current_bit:
                answer[el] |= cap2
        new_capacity += 1

    return answer, new_capacity

3 个答案:

答案 0 :(得分:4)

虽然如果没有python中的循环,这可能是不可能的,但是numba并且只是及时编译它可以非常快。我假设您的输入可以很容易地表示为布尔数组,使用struct从二进制文件构造起来非常简单。我实现的方法涉及迭代几个不同的对象,但是仔细选择这些迭代以确保它们是编译器优化的,并且从不做两次相同的工作。第一次迭代使用np.where来定位要删除的所有位的索引。这个特定的功能(以及许多其他功能)由numba编译器优化。然后,我使用这个位索引列表来构建要保留的位片的片段索引。最后一个循环将这些切片复制到一个空的输出数组。

import numpy as np
from numba import jit
from time import time

def binary_mask(num, mask):
    num_nbits = num.shape[0] #how many bits are in our big num
    mask_bits = np.where(mask)[0] #which bits are we deleting
    mask_n_bits = mask_bits.shape[0] #how many bits are we deleting
    start = np.empty(mask_n_bits + 1, dtype=int) #preallocate array for slice start indexes
    start[0] = 0 #first slice starts at 0
    start[1:] = mask_bits + 1 #subsequent slices start 1 after each True bit in mask
    end = np.empty(mask_n_bits + 1, dtype=int) #preallocate array for slice end indexes
    end[:mask_n_bits] = mask_bits #each slice ends on (but does not include) True bits in the mask
    end[mask_n_bits] = num_nbits + 1 #last slice goes all the way to the end
    out = np.empty(num_nbits - mask_n_bits, dtype=np.uint8) #preallocate return array
    for i in range(mask_n_bits + 1): #for each slice 
        a = start[i] #use local variables to reduce number of lookups
        b = end[i]
        c = a - i
        d = b - i
        out[c:d] = num[a:b] #copy slices
    return out

jit_binary_mask = jit("b1[:](b1[:], b1[:])")(binary_mask) #decorator without syntax sugar

###################### Benchmark ########################

bignum = np.random.randint(0,2,1000000, dtype=bool) # 1 million random bits
bigmask = np.random.randint(0,10,1000000, dtype=np.uint8)==9 #delete about 1 in 10 bits

t = time()
for _ in range(10): #10 cycles of just numpy implementation
    out = binary_mask(bignum, bigmask)
print(f"non-jit: {time()-t} seconds")

t = time()
out = jit_binary_mask(bignum, bigmask) #once ahead of time to compile
compile_and_run = time() - t

t = time()
for _ in range(10): #10 cycles of compiled numpy implementation
    out = jit_binary_mask(bignum, bigmask)
jit_runtime = time()-t
print(f"jit: {jit_runtime} seconds")

print(f"estimated compile_time: {compile_and_run - jit_runtime/10}")

在这个例子中,我对一个长度为1,000,000的布尔数组执行基准测试,对于编译和未编译的版本,总共执行10次。在我的笔记本电脑上,输出是:

non-jit: 1.865583896636963 seconds
jit: 0.06370806694030762 seconds
estimated compile_time: 0.1652850866317749

正如您可以通过这样的简单算法看到的,从编译中可以看到非常显着的性能提升。 (在我的情况下,大约20-30倍加速)

答案 1 :(得分:3)

据我所知,只有在M是2的幂时,才能在不使用循环的情况下完成此操作。

我们以你的例子为例,修改M以使它具有2的幂:

N = 0b100011101110 = 2286
M = 0b000000001000 = 8

N中删除第四个最低位并将高位向右移动将导致:

N = 0b10001110110 = 1142

我们使用以下算法实现了这一目标:

  • N = 0b100011101110 = 2286
  • 开头
  • M中最重要的位到最低位重复。
  • 如果M中的当前位设置为1,则将较低位存储在某个变量x中:
    • x = 0b1101110
  • 然后,从M中减去N中的当前位,包括当前位,以便我们最终得到以下结果:
    • N - (0b10000000 + x) = N - (0b10000000 + 0b1101110) = 0b100011101110 - 0b11101110 = 0b100000000000
    • 此步骤也可以通过使用0来实现,这可能更有效。
  • 接下来,我们将结果向右移一次:
    • 0b100000000000 >> 1 = 0b10000000000
  • 最后,我们将x添加回移位结果:
    • 0b10000000000 + x = 0b10000000000 + 0b1101110 = 0b10001101110 = 1142

如果你只是简单地迭代M(从最重要的位到最低有效位)并在每个设置位上执行此过程,因为时间复杂度为O(M.bit_length())

我也写了这个算法的代码,我相信它相对有效,但我没有任何大的二进制数来测试它:

def remove_bits(N, M):
    bit = 2 ** (M.bit_length() - 1)

    while bit != 0:
        if M & bit:
            ones = bit - 1

            # Store lower `bit` bits.
            temp = N & ones

            # Clear lower `bit` bits.
            N &= ~ones

            # Shift once to the right.
            N >>= 1

            # Set stored lower `bit` bits.
            N |= temp

        bit >>= 1

    return N

if __name__ == '__main__':
    N = 0b100011101110
    M = 0b000010001000

    print(bin(remove_bits(N, M)))

使用您的示例,返回结果:0b1000110110

答案 2 :(得分:2)

我认为在内置的按位运算符的常数调用中没有任何方法可以做到这一点。为了实现这一点,Python必须提供类似PEXT的东西。

对于数以百万计的数字,您实际上可以通过在位序列方面工作,牺牲Python整体的空间优势以及按位操作的时间优势来获得最佳性能,从而有利于您可以执行的操作的更大灵活性。我不知道盈亏平衡点在哪里:

import itertools

bits = bin(N)[2:]
maskbits = bin(M)[2:].zfill(len(bits))
bits = bits.zfill(len(maskbits))

chosenbits = itertools.compress(bits, map('0'.__eq__, maskbits))

result = int(''.join(chosenbits), 2)