brotli的官方代码,提供了各种语言的封闭,包括java、go、js、python等。python的封装是先编译一个_brotli.so模块,再有一个brotli.py进行加载,这个封装大体有如下一些问题:
1. 内部buffer使用的是c++的vector<uint8_t>,额外链接了libc++库,不是个大问题,但是感觉没有必要
2. 无论压缩还是解压,只支持bytes输入,不支持memoryview,往往这带来额外的内存拷贝
3. 返回结果时,需要再从内部的vector<uint8_t>再转换成PyBytes对象,其实也可以直接拿PyBytes当buffer,来减少拷贝
4. 如果直接一个brotli.so更好,而不是一个python再来加载_brotli.so
为解决以上问题,对brotli进行重新python封装,抛弃原来的,使用cython进行重写。过程中,大体遇到如下一些问题。
直接拿ByBytes来当buffer使用,可以直接返回,这样减少了内存拷贝,当然这样是需要需要占用gil的,了解python的gil原理就知道,这样对多线程不友好。
使用PyBytes_FromStringAndSize()来创建一个PyBytes对象,然后再使用_PyBytes_Resize()来进行空间扩展。这时有个有趣的问题,就是PyBytes_FromStringAndSize(NULL, 0)创建的空bytes,其引用不为1,而_PyBytes_Resize()内置函数需要引用为1才能使用,以避免安全问题。所以需要特殊处理一下。
再来就是,走了个大弯路,cython里对所有Python c函数进行了重新定义,即PyBytes_FromStringAndSize()返回的是一个bytes,而不是PyObject*指针。这时问题就出现了:一旦有了外部引用之后,再调用_PyBytes_Resize()就会导致问题,因为扩展内存时,会自动释放原来的内存,外面引用的对象会在python回收内存时,crash掉。
那就想办法绕过去:
cdef PyObject *ptr = <PyObject*>PyBytes_FromStringAndSize()
这样也是行不通的,在cython转换时c代码时就会报warning了,这样做还是产生了一个临时的bytes对象,而我需要的是直接产生一个未被引用的PyObject*指针。
折腾一番,还是得直接写c代码,直接调用原始的python的PyBytes_FromStringAndSize(),然后再定义pxd头文件,再在cython的pyx中cimport进来。这时遇到的麻烦就是raise Exception的问题,用c代码来定义一个Exception类太麻烦,那就妥协一下,pxd里定义的函数,会返回错误码,在pyx里再把错误码转换成对应的Exception抛出。
为减少内存拷贝,最好的办法是直接向压缩函数里传递一个const char*指针和一个长度,当然也需要兼容原来的直接传一个bytes对象。即在cython里要支持两种写法就绝佳了:
cdef bytes data
brotli.decompress(data)
cdef int len
cdef const char *base
brotli.decompress(<uint8_t[:len]>base)
方式二传递的是一个array对象,cython里介绍了安全的使用办法,后来发现使用时可以跟bytes的使用方法统一,便是使用cython的memoryview写法:
cdef const uint8_t[:] memview = input #web.array or bytes
cdef const uint8_t *ptr = <const uint8_t*>&memview[0]
brotli的默认压缩级别为11,很慢,使用时需要设置合适等级。
brotli流式压缩时,BrotliEncoderCompressStream需要BROTLI_OPERATION_PROCESS + BROTLI_OPERATION_FLUSH或者BROTLI_OPERATION_PROCESS + BROTLI_OPERATION_FINISH,在c代码里实现这些也能减少结果拼接时的内存拷贝,而官方封装便是在python里直接相加来拼接。
压缩的末尾标记brotli是2字节,而 gzip里印象中是10字节,要小得多。对末尾标记的内容空间预留处理,也能减少不必要的内存拷贝。
重新封装后的brotli使用更方便,效率也更快(大约5%左右)。