Python - 如何以最高效的方式对“bytes”中的每个字节进行“xor”操作

10
我有一个“bytes”对象和一个整数掩码,“bytes”对象中的所有字节都要与掩码进行异或操作。我需要在较大的“bytes”对象(约4096 KB)上重复执行此操作。
这是我编写的代码,它可以正常工作,但占用CPU很高,导致我的脚本运行缓慢:
# 'data' is bytes and 'mask' is int
bmask = struct.pack('!I', mask) # converting the "int" mask to "bytes" of 4 bytes 
a = bytes(b ^ m for b, m in zip(data, itertools.cycle(bmask)))

我能想到的最好方案是这个,速度快了大约20倍:

# 'data' is bytes and 'mask' is int
# reversing the bytes of the mask
bmask = struct.pack("<I", mask)
mask = struct.unpack(">I", bmask)[0]

# converting from bytes to array of "int"s
arr = array.array("I", data)

# looping over the "int"s
for i in range(len(arr)):
    arr[i] ^= mask

# must return bytes
a = bytes(arr)

我的问题是:

  1. 有没有更有效率的方法来完成这个任务(在CPU方面)?
  2. 有没有更“清晰”的方法来完成这个任务(不影响性能)?

附言:如果有任何重要性,我正在使用Python 3.5。


data是什么?它是列表、字节、迭代器还是其他什么? - Ecir Hana
如果它是一个瓶颈,编写一个从Python调用的C函数可能是有意义的。 - Serge Ballesta
"数据"是字节,我会更新问题。 - Gil Barash
2个回答

3

我认为使用纯Python编写的算法已经无法更快了(但Fabio Veronese的回答表明这不是真的)。你可以通过在列表推导式中循环来稍微缩短一点时间,但是那个列表需要转换回数组,而数组必须转换为字节,因此,为了获得微不足道的好处而使用更多的RAM。

然而,你可以通过使用Numpy使其变得更快。以下是一个简短的演示。

from time import perf_counter
from random import randrange, seed
import array
import numpy as np

seed(42)

def timed(func):
    ''' Timing decorator '''
    def wrapped(*args):
        start = perf_counter()
        result = func(*args)
        stop = perf_counter()
        print('{}: {:.6f} seconds'.format(func.__name__, stop - start))
        return result
    wrapped.__name__ = func.__name__
    wrapped.__doc__ = func.__doc__
    return wrapped

@timed
def do_mask_arr1(data, mask):
    arr = array.array("I", data)
    # looping over the "int"s
    for i in range(len(arr)):
        arr[i] ^= mask
    return arr.tobytes()

@timed
def do_mask_arr2(data, mask):
    arr = array.array("I", data)
    return array.array("I", [u ^ mask for u in arr]).tobytes()

@timed
def do_mask_numpy(data, mask):
    return (np.fromstring(data, dtype=np.uint32) ^ mask).tobytes()

@timed
def make_data(datasize):
    ''' Make some random bytes '''
    return bytes(randrange(256) for _ in range(datasize))

datasize = 100000
mask = 0x12345678
data = make_data(datasize)

d1 = do_mask_arr1(data, mask)
d2 = do_mask_arr2(data, mask)
print(d1 == d2)

d3 = do_mask_numpy(data, mask)
print(d1 == d3)

典型输出
make_data: 0.751557 seconds
do_mask_arr1: 0.026865 seconds
do_mask_arr2: 0.025110 seconds
True
do_mask_numpy: 0.000438 seconds
True

在一台旧的单核32位2GHz Linux机器上使用Python 3.6.0进行测试。

我刚刚做了一次运行, datasize = 4000000 do_mask_numpy 花费了0.0422秒。


实际上,使用Python仍然可以获得更好的结果,但是numpy仍然是最快的。 - Fabio Veronese

2

如果您不想使用numpy,这是一种替代方法。优点在于只需进行一次比较,同时将掩码大小扩展到所需大小(取决于数据大小)。

@timed
def do_mask_int(data, mask):
    intdata = int.from_bytes(data, byteorder='little', signed=False)
    strmask = format(mask,'0x')
    strmask = strmask * ((intdata.bit_length() + 31) // 32)
    n = intdata ^ int(strmask, 16)
    return n.to_bytes(((n.bit_length() + 7) // 8), 'little') or b'\0'

结果如下:

make_data: 8.288754 seconds
do_mask_arr1: 0.258530 seconds
do_mask_arr2: 0.253095 seconds
True
do_mask_numpy: 0.010309 seconds
True
do_mask_int: 0.060408 seconds
True

仍然要感谢numpy速度更快,但也许有人不想将其包含在生产环境中。 :]最好的

非常聪明!您可以通过将那些除法替换为移位操作来加快速度:a // 32 -> a >> 5a // 8 -> a >> 3 - PM 2Ring
make_data: 8.696361 秒 do_mask_arr1: 0.260604 秒 do_mask_arr2: 0.207433 秒 True do_mask_numpy: 0.006003 秒 True do_mask_int: 0.050470 秒 True - Fabio Veronese

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接