zoukankan      html  css  js  c++  java
  • Python使用Zero-Copy和Buffer Protocol实现高性能编程

    无论你程序是做什么的,它经常都需要处理大量的数据。这些数据大部分表现形式为strings(字符串)。然而,当你对字符串大批量的拷贝,切片和修改操作时是相当低效的。为什么?

    让我们假设一个读取二进制数据的大文件示例,然后将部分数据拷贝到另外一个文件。要展示该程序所使用的内存,我们使用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。

    这里有趣的是在哪里呢,也就是构建content_to_write时10 MB的程序内存增长。切片操作拷贝了除了开头的一个单位的KB其他所有的数据到一个新的字符串对象。

    如果处理类似大量的字节数组对象操作那是简直就是灾难。如果你之前写过C语言,在使用memcpy()需要注意点是:在内存使用以及总体性能来说,复制内存很慢。

    然而,作为C程序员的你,知道字符串其实就是由字符数组构成,你不非得通过拷贝也能只处理部分字符,通过使用基本的指针运算——只需要确保整个字符串是连续的内存区域。

    在Python同样提供了buffer protocol实现。buffer protocol定义在PEP 3118,描述了使用C语言API实现各种类型的支持,例如字符串。

    当一个对象实现了该协议,你就可以使用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码

    在上面的例子中,在使用memoryview对象的切片操作,同样返回一个memoryview对象。意味着它并没有拷贝任何数据,而是通过引用部分数据实现的。

    下面图示解释发生了什么:

    alt

    因此,我们可以将之前的程序改造得更加高效。我们需要使用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'x00x00x00x00x00x00x00x00')
    >>> with open("/dev/urandom", "rb") as source:
    ...     source.readinto(ba)
    ... 
    8
    >>> ba
    bytearray(b'`m.zx8dx0fpxa1')
    

    通过该机制,我们可以很简单写入到预定义的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'x00x00x00x00x0bx19xaexb2')
    

    buffer protocol是实现低内存开销的基础,具备很强的性能。虽然Python隐藏了所有的内存分配,开发者不需要关系内部是怎么样实现的。

    可以再去了解一下array模块和struct模块是如何处理buffer protocol的,zero copy操作是相当高效的。

  • 相关阅读:
    个人作业十六:找水王
    个人作业十四:第一阶段个人冲刺
    个人作业十三:用户场景描述
    个人作业十二:最大子数组三
    个人作业十一:敏捷开发
    个人作业十:返回子数组二
    个人作业九:返回数组中最大子数组的和
    个人作业八:四则运算四
    个人作业七:四则运算三
    linux系统RAID
  • 原文地址:https://www.cnblogs.com/erhuabushuo/p/10314803.html
Copyright © 2011-2022 走看看