不管你程序是作什么的,它常常都须要处理大量的数据。这些数据大部分表现形式为strings(字符串)。然而,当你对字符串大批量的拷贝,切片和修改操做时是至关低效的。为何?python
让咱们假设一个读取二进制数据的大文件示例,而后将部分数据拷贝到另一个文件。要展现该程序所使用的内存,咱们使用memory_profiler,一个强大的Python包,让咱们能够一行一行观察程序所使用的内存。程序员
@profile def read_random(): with open("/dev/urandom", "rb") as source: content = source.read(1024 * 10000) content_to_write = content[1024:] print(f"content length: {len(content)}, content to write length {len(content_to_write)}") with open("/dev/null", "wb") as target: target.write(content_to_write) if __name__ == "__main__": read_random()
使用memory_profiler模块来执行以上程序,输出以下:数组
$ python -m memory_profiler example.py content length: 10240000, content to write length 10238976 Filename: example.py Line # Mem usage Increment Line Contents ================================================ 1 14.320 MiB 14.320 MiB @profile 2 def read_random(): 3 14.320 MiB 0.000 MiB with open("/dev/urandom", "rb") as source: 4 24.117 MiB 9.797 MiB content = source.read(1024 * 10000) 5 33.914 MiB 9.797 MiB content_to_write = content[1024:] 6 33.914 MiB 0.000 MiB print(f"content length: {len(content)}, content to write length {len(content_to_write)}") 7 33.914 MiB 0.000 MiB with open("/dev/null", "wb") as target: 8 33.914 MiB 0.000 MiB target.write(content_to_write)
咱们经过source.read从/dev/unrandom加载了10 MB数据。Python须要大概须要分配10 MB内存来以字符串存储这个数据。以后的content[1024:]指令越过开头的一个单位的KB数据进行数据拷贝,也分配了大概10 MB。dom
这里有趣的是在哪里呢,也就是构建content_to_write时10 MB的程序内存增加。切片操做拷贝了除了开头的一个单位的KB其余全部的数据到一个新的字符串对象。socket
若是处理相似大量的字节数组对象操做那是简直就是灾难。若是你以前写过C语言,在使用memcpy()须要注意点是:在内存使用以及整体性能来讲,复制内存很慢。函数
然而,做为C程序员的你,知道字符串其实就是由字符数组构成,你不非得经过拷贝也能只处理部分字符,经过使用基本的指针运算——只须要确保整个字符串是连续的内存区域。性能
在Python一样提供了buffer protocol实现。buffer protocol定义在PEP 3118,描述了使用C语言API实现各类类型的支持,例如字符串。ui
当一个对象实现了该协议,你就可使用memoryview类构造一个memoryview对象引用原始内存对象。指针
>>> s = b"abcdefgh" >>> view = memoryview(s) >>> view[1] 98 >>> limited = view[1:3] >>> limited <memory at 0x7f6ff2df1108> >>> bytes(view[1:3]) b'bc'
注意:98是字符b的ACSII码code
在上面的例子中,在使用memoryview对象的切片操做,一样返回一个memoryview对象。意味着它并无拷贝任何数据,而是经过引用部分数据实现的。
下面图示解释发生了什么:
所以,咱们能够将以前的程序改造得更加高效。咱们须要使用memoryview对象来引用数据,而不是开辟一个新的字符串。
@profile def read_random(): with open("/dev/urandom", "rb") as source: content = source.read(1024 * 10000) content_to_write = memoryview(content)[1024:] print(f"content length: {len(content)}, content to write length {len(content_to_write)}") with open("/dev/null", "wb") as target: target.write(content_to_write) if __name__ == "__main__": read_random()
咱们再一次使用memory profiler执行上面程序:
$ python -m memory_profiler example.py content length: 10240000, content to write length 10238976 Filename: example.py Line # Mem usage Increment Line Contents ================================================ 1 14.219 MiB 14.219 MiB @profile 2 def read_random(): 3 14.219 MiB 0.000 MiB with open("/dev/urandom", "rb") as source: 4 24.016 MiB 9.797 MiB content = source.read(1024 * 10000) 5 24.016 MiB 0.000 MiB content_to_write = memoryview(content)[1024:] 6 24.016 MiB 0.000 MiB print(f"content length: {len(content)}, content to write length {len(content_to_write)}") 7 24.016 MiB 0.000 MiB with open("/dev/null", "wb") as target: 8 24.016 MiB 0.000 MiB target.write(content_to_write)
在该程序中,source.read仍然分配了10 MB内存来读取文件内容。然而,使用memoryview来引用部份内容时,并无额外在分配内存。
相比以前的版本,这里节省了大概50%的内存开销。
该技巧,在处理sockets通讯的时候极其有用。当经过socket发送数据时,全部的数据可能并无在一次调用就发送。
import socket s = socket.socket(…) s.connect(…) # Build a bytes object with more than 100 millions times the letter `a` data = b"a" * (1024 * 100000) while data: sent = s.send(data) # Remove the first `sent` bytes sent data = data[sent:] <2>
使用以下实现,程序一次次拷贝直到全部的数据发出。经过使用memoryview,能够实现zero-copy(零拷贝)方式来完成该工做,具备更高的性能:
import socket s = socket.socket(…) s.connect(…) # Build a bytes object with more than 100 millions times the letter `a` data = b"a" * (1024 * 100000) mv = memoryview(data) while mv: sent = s.send(mv) # Build a new memoryview object pointing to the data which remains to be sent mv = mv[sent:]
在这里就不会发生任何拷贝,也不会在给data分配了100 MB内存以后再分配多余的内存来进行屡次发送了。
目前,咱们经过使用memoryview对象实现高效数据写入,但在某些状况下读取也一样适用。在Python中大部分 I/O 操做已经实现了buffer protocol机制。在本例中,咱们并不须要memoryview对象,我能够请求 I/O 函数写入咱们预约义好的对象:
>>> ba = bytearray(8) >>> ba bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00') >>> with open("/dev/urandom", "rb") as source: ... source.readinto(ba) ... 8 >>> ba bytearray(b'`m.z\x8d\x0fp\xa1')
经过该机制,咱们能够很简单写入到预约义的buffer中(在C语言中,你可能须要屡次调用malloc())。
适用memoryview,你甚至能够将数据放入到内存区域任意点:
>>> ba = bytearray(8) >>> # Reference the _bytearray_ from offset 4 to its end >>> ba_at_4 = memoryview(ba)[4:] >>> with open("/dev/urandom", "rb") as source: ... # Write the content of /dev/urandom from offset 4 to the end of the ... # bytearray, effectively reading 4 bytes only ... source.readinto(ba_at_4) ... 4 >>> ba bytearray(b'\x00\x00\x00\x00\x0b\x19\xae\xb2')
buffer protocol是实现低内存开销的基础,具有很强的性能。虽然Python隐藏了全部的内存分配,开发者不须要关系内部是怎么样实现的。
能够再去了解一下array模块和struct模块是如何处理buffer protocol的,zero copy操做是至关高效的。