bytes 对象的缓存池

390 阅读3分钟

效率问题

先来探究一下bytes对象的效率,我们知道Python的不可变对象在运算时,处理方式是再创建一个新的。所以三个bytes对象a、b、c在相加时,那么会先根据a + b创建临时对象,然后再根据临时对象+c创建新的对象,最后返回指针。所以:

result = b""
for _ in bytes_list:
    result += _

这是一种效率非常低下的做法,因为涉及大量临时对象的创建和销毁,不仅是这里字节序列,后面要分析的字符串也是同样的道理。

官方推荐的做法是,使用join,字符串和字节序列都可以对一个列表进行join,将列表里面的多个字符串或者字节序列join在一起。

举个Python中的例子,我们以字符串为例,字节序列同样如此:

def bad():
    s = ""
    for _ in range(1, 10):
        s += str(_)
    return s

def good():
    l = []
    for _ in range(1, 10):
        l.append(str(_))
    return "".join(l)

def better():
    return "".join(str(_) for _ in range(1, 10))

def best():
    return "".join(map(str, range(1, 10)))

字节序列缓存池

为了优化单字节bytes对象的创建效率,Python底层维护了一个缓存池,该缓存池是一个PyBytesObject*类型的数组。

static PyBytesObject *characters[UCHAR_MAX + 1];

Python内部创建单字节bytes对象时,先检查目标对象是否已在缓存池中。PyBytes_FromStringAndSize函数是负责创建bytes对象的一个常用的Python/C API,位于Objects/bytesobject.c中:

PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{  
    //PyBytesObject对象的指针
    PyBytesObject *op;
    if (size < 0) {
        //显然size不可以小于0
        PyErr_SetString(PyExc_SystemError,
            "Negative size passed to PyBytes_FromStringAndSize");
        return NULL;
    }
    //如果size为1,表明创建的是单字节对象
    //当然str不可以为NULL, 而且获取到的字节必须要在characters里面
    if (size == 1 && str != NULL &&
        (op = characters[*str & UCHAR_MAX]) != NULL)
    {
#ifdef COUNT_ALLOCS
        _Py_one_strings++;
#endif  
        //增加引用计数,返回指针
        Py_INCREF(op);
        return (PyObject *)op;
    }
  
    //否则话创建新的PyBytesObject,此时是个空
    op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
    if (op == NULL)
        return NULL;
    if (str == NULL)
        return (PyObject *) op;
  
    //不管size是多少,都直接拷贝即可
    memcpy(op->ob_sval, str, size);
    //但是size是1的话,除了拷贝还会放到缓存池characters中
    if (size == 1) {
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    //返回其指针
    return (PyObject *) op;
}

由此可见,当Python程序开始运行时,字节序列缓存池是空的。但随着单字节bytes对象的创建,缓冲池中的对象慢慢多了起来。

这样一来,单字节序列首次创建后便在缓存池中缓存起来;后续再次使用时, Python直接从缓存池中取,避免重复创建和销毁。字节序列缓存池也只能容纳为数不多的 256 个单字节序列,但使用频率非常高。

缓冲池技术作为一种以时间换空间的优化手段,只需较小的内存为代价,便可明显提升执行效率。

>>> a1 = b"a"
>>> a2 = b"a"
>>> a1 is a2
True
>>>
>>> a1 = b"ab"
>>> a2 = b"ab"
>>> a1 is a2
False
>>>

显然此时不需要解释了,单字节bytes对象会缓存起来,不是单字节则不会缓存。

小结

以上就是bytes对象的全部内容,我们说:

  • bytes对象是一个变长、不可变对象,内部的值是通过一个C的字符数组来维护的;
  • bytes也是序列型操作,它支持的操作在bytes_as_sequence和bytes_as_mapping中;
  • Python内部通过维护字节序列缓存池来优化单字节bytes对象的创建和销毁操作;
  • 缓存池是一种常用的以空间换时间的优化技术;

最后,Python除了bytes对象之外,还有一个bytearray对象,它的表现和bytes对象是完全一致的,但一个是可变对象、一个是不可变对象。bytearray对象底层对应的结构体是PyByteArrayObject,其相关操作、以及类型对象都位于bytearrayobject.c中,有兴趣可以看一下。

以上就是本次分享的所有内容,想要了解更多欢迎前往公众号:Python编程学习圈,每日干货分享