一个配置表优化的想法

今天下班在班车上想了一个关于配置表存储的小优化,起因是早上的时候发现了一个bug,这个bug是由于在运行时动态更改了一个列表配置导致的。

其实关于这种运行时“偷偷”改配置的问题我之前也有考虑过,这种应该是一不小心就会写出的,这不终于都出了一个。

至于如何预防这种问题,我认为在python里面似乎也没有什么好的解决方法,因为它不像c++有const语义,但有一个稍尽人事的预防措施就是把列表型的配置读成元组(tuple)。而由此衍生出的一个想法便是:把配置表中所有的列表型配置都读成共享的元组,即只要是其内容一致的那么内存中就只会存一份,类似于string intern

在将这个想法付诸实践之前,有一些问题是需要搞清楚的。因为如果我想要做的,python都帮我做的七七八八了,那我就不需要在此花费无用的精力了。因此首要问题是,python的元组是否有共享机制?以及其对何种元组是会共享的?

显然不可能是所有的元组都使用共享策略,但我知道空的元组是会被共享的。

 

稍微阅读了一下python的元组源码,在tupleobject.c中

 

空闲链表:

/* Speed optimization to avoid frequent malloc/free of small tuples */
#ifndef PyTuple_MAXSAVESIZE
#define PyTuple_MAXSAVESIZE     20  /* Largest tuple to save on free list */
#endif
#ifndef PyTuple_MAXFREELIST
#define PyTuple_MAXFREELIST  2000  /* Maximum number of tuples of each size to save */
#endif

#if PyTuple_MAXSAVESIZE > 0
/* Entries 1 up to PyTuple_MAXSAVESIZE are free lists, entry 0 is the empty
   tuple () of which at most one instance will be allocated.
*/
static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
static int numfree[PyTuple_MAXSAVESIZE];
#endif

 

创建元组的代码:

PyObject *
PyTuple_New(register Py_ssize_t size)
{
    register PyTupleObject *op;
    Py_ssize_t i;
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
#if PyTuple_MAXSAVESIZE > 0
    if (size == 0 && free_list[0]) {
        op = free_list[0];
        Py_INCREF(op);
#ifdef COUNT_ALLOCS
        tuple_zero_allocs++;
#endif
        return (PyObject *) op;
    }
    if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
        free_list[size] = (PyTupleObject *) op->ob_item[0];  /*从空闲链表中取一个长度相等的空闲的元组*/
        numfree[size]--;
#ifdef COUNT_ALLOCS
        fast_tuple_allocs++;
#endif
        /* Inline PyObject_InitVar */
#ifdef Py_TRACE_REFS
        Py_SIZE(op) = size;
        Py_TYPE(op) = &PyTuple_Type;
#endif
        _Py_NewReference((PyObject *)op);
    }
    else
#endif
    {
        Py_ssize_t nbytes = size * sizeof(PyObject *);
        /* Check for overflow */
        if (nbytes / sizeof(PyObject *) != (size_t)size ||
            (nbytes > PY_SSIZE_T_MAX - sizeof(PyTupleObject) - sizeof(PyObject *)))
        {
            return PyErr_NoMemory();
        }

        op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
        if (op == NULL)
            return NULL;
    }
    for (i=0; i < size; i++)
        op->ob_item[i] = NULL;
#if PyTuple_MAXSAVESIZE > 0
    if (size == 0) {
        free_list[0] = op;
        ++numfree[0];
        Py_INCREF(op);          /* extra INCREF so that this is never freed */
    }
#endif
#ifdef SHOW_TRACK_COUNT
    count_tracked++;
#endif
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

 

删除元组:

static void
tupledealloc(register PyTupleObject *op)
{
    register Py_ssize_t i;
    register Py_ssize_t len =  Py_SIZE(op);
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    if (len > 0) {
        i = len;
        while (--i >= 0)
            Py_XDECREF(op->ob_item[i]);
#if PyTuple_MAXSAVESIZE > 0
        if (len < PyTuple_MAXSAVESIZE &&
            numfree[len] < PyTuple_MAXFREELIST &&
            Py_TYPE(op) == &PyTuple_Type)
        {
            op->ob_item[0] = (PyObject *) free_list[len]; /*先不回收内存,将该元组链入空闲链表*/
            numfree[len]++;
            free_list[len] = op;
            goto done; /* return */
        }
#endif
    }
    Py_TYPE(op)->tp_free((PyObject *)op);
done:
    Py_TRASHCAN_SAFE_END(op)
}

 

通过上述代码可以发现,在CPython中实际上只会对空的元组使用共享策略。并且空的元组只要创建了就永远不会回收其内存,因为为其引用计数特别的+1,导致空元组的引用计数永远不会为0。

而对于小的元组(长度小于PyTuple_MAXSAVESIZE=20),python有池机制。当这类元组触发回收时,会先考虑将其链入一个空闲链表。具体操作是,先根据元组的长度,找到对应的空闲链表,然后查看这个空闲链表的当前已经有多少个元组(数组numfree记录链表元素个数),如果未超过上限(上限为PyTuple_MAXFREELIST=2000),则将其链入,否则直接释放其内存。

这样看来,python是几乎没有元组的共享策略。

posted @ 2017-09-06 23:02  adinosaur  阅读(277)  评论(0编辑  收藏  举报