Python中高效交换字节的方法

6

我有一些长度为2*nbytearray

a1 a2 b1 b2 c1 c2

我需要将每个2字节的单词中的字节顺序进行转换,并使之变为:

a2 a1 b2 b1 c2 c1

现在我使用的方法很慢,不适合我的任务:

converted = bytearray([])
for i in range(int(len(chunk)/2)):
   converted += bytearray([ chunk[i*2+1], chunk[i*2] ])

有没有可能通过调用某些系统/libc函数来切换bytearray的大小端?


好的,谢谢大家,我记录了一些建议:

import timeit

test = [
"""
converted = bytearray([])
for i in range(int(len(chunk)/2)):
   converted += bytearray([ chunk[i*2+1], chunk[i*2] ])
""",
"""
for i in range(0, len(chunk), 2):
    chunk[i], chunk[i+1] = chunk[i+1], chunk[i]
""",
"""
byteswapped = bytearray([0]) * len(chunk)
byteswapped[0::2] = chunk[1::2]
byteswapped[1::2] = chunk[0::2]
""",
"""
chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]
"""
]

for t in test:
    print(timeit.timeit(t, setup='chunk = bytearray([1]*10)'))

并且结果是:

$ python ti.py
11.6219761372
2.61883187294
3.47194099426
1.66421198845

现在针对步长为2的切片赋值已经是最快的了。同时感谢F先生提供的详细解释,但由于涉及到numpy,我尚未尝试。

5个回答

8
你可以使用步长为2的切片赋值:
byteswapped = bytearray(len(original))
byteswapped[0::2] = original[1::2]
byteswapped[1::2] = original[0::2]

如果你想要在原地进行操作:

original[0::2], original[1::2] = original[1::2], original[0::2]

通过测试发现,对于较大的数组,在Python级别循环与切片两种方式中,切片的性能表现更为优异:

>>> timeit.timeit('''
... for i in range(0, len(chunk), 2):
...     chunk[i], chunk[i+1] = chunk[i+1], chunk[i]''',
... 'chunk=bytearray(1000)')
81.70195105159564
>>>
>>> timeit.timeit('''
... byteswapped = bytearray(len(original))
... byteswapped[0::2] = original[1::2]
... byteswapped[1::2] = original[0::2]''',
... 'original=bytearray(1000)')
2.1136113323948393
>>>
>>> timeit.timeit('chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]', 'chunk=
bytearray(1000)')
1.79349659994989

对于小型数组,切片仍然优于显式循环,但差距不是很大:

>>> timeit.timeit('''
... for i in range(0, len(chunk), 2):
...     chunk[i], chunk[i+1] = chunk[i+1], chunk[i]''',
... 'chunk=bytearray(10)')
1.2503637694328518
>>>
>>> timeit.timeit('''
... byteswapped = bytearray(len(original))
... byteswapped[0::2] = original[1::2]
... byteswapped[1::2] = original[0::2]''',
... 'original=bytearray(10)')
0.8973060929306484
>>>
>>> timeit.timeit('chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]', 'chunk=
bytearray(10)')
0.6282232971918802

@user3479125:我已经纠正了代码中创建“byteswapped”数组的低效性;如果您重新计时,它现在应该优于显式循环。我还添加了一种基于切片的原地解决方案,应该更快。此外,如果您尝试使用较大的数组进行计时,您会发现由于解释器开销更大,显式循环的比例要比切片差得多。 - user2357112

3
我可以重复@user2357112支持Monica的就地切片方法的结果。 但是,如果数据大小增加十倍,则与array.byteswap()相比速度会变慢两倍:
>>> timeit.timeit('chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]', 'chunk=bytearray(10000)')
12.54664899999625

>>> timeit.timeit('a=array.array("H",chunk);a.byteswap();chunk=a.tobytes()', 'import array;chunk=bytearray(10000)')
6.398380300001008

import array 的影响是否相关? - AcK
@ack 不需要。因为当我们努力优化时,我们可能会将影响与许多循环平均。当import array移动到循环中时,在1000个案例中,影响仅有几个百分点。实际上,我认为chunk=a.tobytes()不必要存在(可以节省10%)。 - mh001

2
如果使用numpy的frombuffer函数,您可以构建一个numpy ndarray,它实际上共享了bytearray的物理内存,然后交换操作可以原地完成而不是在副本中进行。
您可以直接在byt上执行相同的索引操作,例如
byt[i] = byt[i+1]

但是在numpy中,通过显式类型获取缓冲数组通常可以让您做更多的事情,特别是对于bytearray,因为它本身非常有限。

不过要小心。尽管在Python级别上,bytearray表示无符号8位整数值(0-255),但实际的底层C实现,在bytearrayobject.h中使用普通的char作为字节值(请参阅此处了解更多信息)。如果要使用numpy进行操作,您可能需要使用frombuffer的可选dtype参数,并将其设置为dtype=numpy.ubyte

import numpy as np
byt = bytearray(range(256))
npbyt = np.frombuffer(byt, dtype=np.ubyte)

for i in range(0, len(npbyt)-1, 2):
    temp = npbyt[i]
    npbyt[i] = npbyt[i+1]
    npbyt[i+1] = temp

print(list(byt))

它会打印出来

[1,  0, 3, 2, 5, 4, ... 255, 254] 

我在做一个名为的小项目时遇到了一些问题,它可以对实现可写缓冲区协议的Python对象(如bytearray)进行原地排序。
如果您有兴趣从那里获取Cython源代码,那里有一个简单的辅助函数_swap,可以轻松完成您想要的操作。但这对于您的使用情况可能过于复杂。

2

以下是一些在如何在Python中对32位整数进行字节交换?问题的回答。

import struct

def htonl_slice(data):
    byteswapped = bytearray(4)
    byteswapped[0::4] = data[3::4]
    byteswapped[1::4] = data[2::4]
    byteswapped[2::4] = data[1::4]
    byteswapped[3::4] = data[0::4]
    return byteswapped

def htonl_slice_2(data):
    byteswapped = bytearray(len(data))
    byteswapped[0::4] = data[3::4]
    byteswapped[1::4] = data[2::4]
    byteswapped[2::4] = data[1::4]
    byteswapped[3::4] = data[0::4]
    return byteswapped

def htonl_struct(data):
    return struct.pack(">L", struct.unpack("<L", data)[0])

def swap32(data):
    return [struct.unpack("<I", struct.pack(">I", i))[0] for i in data]

def htonl_shift(x):
    return (((x << 24) & 0xFF000000) |
            ((x <<  8) & 0x00FF0000) |
            ((x >>  8) & 0x0000FF00) |
            ((x >> 24) & 0x000000FF))    

def test(self):

    data = [struct.pack('<L', i) for i in range(1000000)]

    start = time.time()
    for d in data:
        x = htonl_slice(d)
    end = time.time()
    print("htonl_slice %f" % (end - start))
    
    start = time.time()
    for d in data:
        x = htonl_struct(d)
    end = time.time()
    print("htonl_struct %f" % (end - start))

    data = [i for i in range(1000000)]
    start = time.time()
    for d in data:
        x = htonl_shift(d)
    end = time.time()
    print("htonl_shift %f" % (end - start))

    start = time.time()
    x = swap32(data)
    end = time.time()
    print("swap32 %f" % (end - start))

    data = bytearray()
    for i in range(1000000):
        data += struct.pack('<L', i)

    start = time.time()
    x = htonl_slice_2(data)
    end = time.time()
    print("htonl_slice_2 %f" % (end - start))

结果如下:

htonl_slice   3.041000
htonl_struct  0.626000
htonl_shift   0.864000
swap32        0.533000
htonl_slice_2 0.025000

我知道htonl_shift使用的是int而不是字节数组。

有趣的是,swap32非常快,但对于一个4MB的数组来说,htonl_slice_2是迄今为止最快的。


1
你也可以在原地交换它们并使用原始数组。
chunk = bytearray([1,2,3,4])

for i in range(0, len(chunk), 2):
    chunk[i], chunk[i+1] = chunk[i+1], chunk[i]

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