鉴于Python的缓冲协议的memoryview
接口可以帮助减少制作临时数据副本的需要,我决定基于此answer对此question进行快速测试。
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
前两个结果看起来非常令人惊讶。我理解调用列表将涉及数据的副本,但我认为在切片内存视图而不是基础字节数组时,您可以保存临时副本。
您不能像使用C或C ++那样考虑Python。额外副本的常量因子开销远远低于支持所有Python动态特性所涉及的常量因子开销,特别是在CPython中没有JIT时。一旦考虑到必须更改的其他内容以避免该副本,您不能假设保存一个副本实际上会有所帮助。
在这种情况下,几乎所有的工作都在列表转换中。你要保存的副本毫无意义。比较b[i:]
和list(b[i:])
的时间,你会看到即使切片执行复制,切片也只是运行时的百分之几。
你保存的副本无关紧要,因为它基本上只是一个memcpy
。相反,列表转换需要在bytestring或memoryview上创建一个迭代器,重复调用迭代器的tp_iternext
槽,获得对应于内存的原始字节等的int
对象,这样更昂贵。对于memoryview来说,它甚至更昂贵,因为memoryview对象必须支持多维形状和非字节数据类型,并且因为memoryview实现没有专用的__iter__
实现,所以它通过基于通用序列的回退迭代,比较慢
您可以使用memoryview的tolist
方法而不是调用list
来节省一些时间。这会跳过一堆迭代协议开销,并允许一次检查,而不是每个项目一次。在我的测试中,这几乎与在字节串上调用list
一样快。