python memoryview比预期的慢

时间:2019-04-20 11:07:04

标签: python memoryview

鉴于Python的memoryview缓冲协议接口可以帮助减少制作临时数据的需求,因此我决定根据此answer对此{{3 }}。

import time

expressions = ['list(b[i:i+1000])',
               'list(b[i:])',
               'b[i:]'
              ]

size = 1000000
x = b'x'*size
mv = memoryview(x)
for e in expressions:
    print(f"Expression: {e}")
    for b in (x, mv):
        l = len(b)
        start = time.time()
        for i in range(0, l, 1000):
            eval(e)
        end = time.time()
        print(f"Size: {size}, {type(b).__name__}, time: {end-start}")

结果:

$ python c:\temp\test_memoryview.py
Expression: list(b[i:i+1000])
Size: 1000000, bytes, time: 0.021999597549438477
Size: 1000000, memoryview, time: 0.03600668907165527
Expression: list(b[i:])
Size: 1000000, bytes, time: 5.3010172843933105
Size: 1000000, memoryview, time: 11.202003479003906
Expression: b[i:]
Size: 1000000, bytes, time: 0.2990117073059082
Size: 1000000, memoryview, time: 0.006985902786254883

前两个结果似乎令人惊讶。 我知道调用列表将涉及数据的副本,但是我认为在切片内存视图而不是基础字节数组时,您会保存一个临时副本。

1 个答案:

答案 0 :(得分:1)

您无法像C或C ++那样想到Python。额外副本的常量因子开销远低于支持所有Python动态特性所涉及的常量因子开销,尤其是在CPython中没有JIT的情况下。一旦考虑到为避免该副本而必须更改的其他内容,就不能认为保存一个副本会真正有所帮助。

在这种情况下,几乎所有工作都在列表转换中。您要保存的副本毫无意义。比较b[i:]list(b[i:])的时间,您将看到切片仅是运行时的百分之几,即使切片执行复制也是如此。

您保存的副本无关紧要,因为它基本上只是一个memcpy。相反,列表转换需要在字节串或memoryview上创建一个迭代器,重复调用迭代器的tp_iternext插槽,获取与内存的原始字节对应的int对象,等等,这是更多的方法昂贵。对于memoryview来说,这甚至更加昂贵,因为memoryview对象必须支持多维形状和非字节数据类型,并且因为memoryview实现没有专用的__iter__实现,所以它通过基于通用序列的后备迭代,速度较慢。

您可以通过使用memoryview的tolist方法而不是调用list来节省时间。这样可以避免一堆迭代协议的开销,并允许仅执行一次检查,而不是每个项目一次。在我的测试中,这几乎与在字节串上调用list一样快。