一个配置表优化的想法
今天下班在班车上想了一个关于配置表存储的小优化,起因是早上的时候发现了一个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是几乎没有元组的共享策略。