写这篇文章的缘由是目前在看《Python源码剖析》[1],可是这本书的做者陈儒老师剖析源码的目的好像不是太明确,因此看上去是为了剖析源码而剖析源码,致使的结果是这本书里面的分析思路不太清楚(多是个人理解问题),并且验证想法的方式是把变量值打印出来,固然这是种很好的方式,但使用调试工具显然更好一点。我读这本书和看源码的目的很简单:为了理解计算机的运行,理解大型软件工程的设计。正如文章的题目为hack python而不是源码阅读,hack是一个理性的分析过程,而阅读不少时候为所欲为的成分多一些。但整体的过程仍是按照书中的顺序来的,这本书很明确的一点就是要作什么不要作什么,这一点我很喜欢。可能会是一个系列,也可能只有这一篇,并不算挖坑。我更但愿从多种视角来审视Python做为一门动态语言的各类特性。做为一个尚未学过编译原理的人来讲这个目标显然很难完成,但正是难完成的东西,才有完成的意义。这篇文章的源码均来自Python-2.5.6[2],全部分析也都是基于此,编译环境是由Koding[3]提供的,还会用到gdb[4]做为调试工具。python
这篇文章主要从源码和运行时的角度观察Python的整形结构。算法
先来看一下PyIntObject的声明[5]:数组
typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
能够看到PyIntObject被声明为一个结构体,包括了Python对象元信息 和一个C语言的long型整数。而Python的Python对象元信息是什么呢?这个问题牵扯到C语言中的宏[6]和Python类型系统的本质[f],先按下不表。数据结构
封装了C语言long型整数的PyIntObject做为数据结构并无什么能让人心潮澎湃的地方,它的迷人之处在于算法[7],也就是PyIntObject的动态组织方式,但是我不可能仅从PyIntObject上管窥到它的组织方式,须要更多的信息来达成这个目的。再来看源码:app
#define BLOCK_SIZE 1000 /* 1K less typical malloc overhead */ #define BHEAD_SIZE 8 /* Enough for a 64-bit pointer */ #define N_INTOBJECTS ((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject)) struct _intblock { struct _intblock *next; PyIntObject objects[N_INTOBJECTS]; }; typedef struct _intblock PyIntBlock; static PyIntBlock *block_list = NULL; static PyIntObject *free_list = NULL;
这段代码对于PyIntObject的组织方式已经说得很清楚了,不用解释。下图形象一点:less
正如前面所说的,这个链表式的数据结构仍是实在太简单,没多少值得把玩的地方。假设我是Python的做者,我会想首先想这门语言出现的缘由,必定是不爽于现有的某些方案,因此才要本身创造新的方案,Python被创造为一种动态类型语言,相比于C之类的静态语言优点在于“动态”二字。但动态不是简单的声明和组织几个数据结构就完事,须要被贯穿到这门语言运行的始终。函数
下面来看一下运行时状态,根据函数名能够确定的是fill_free_list这个函数必然会在很早的时候被调用(来准备须要的内存),咱们先不关注它究竟是怎么作内存分配的,先下个断点,看一下谁第一个调用它,看到第一个触发断点的地方是_PyInt_Init,也就是Python整型对象(类型对象)的初始化函数,推测应该是Python中的每个类型对象都会有一个初始化函数,在Python开始运行时完成初始化工做。来看这个_PyInt_Init函数具体包含了什么内容:工具
int _PyInt_Init(void) { PyIntObject *v; int ival; #if NSMALLNEGINTS + NSMALLPOSINTS > 0 for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) { if (!free_list && (free_list = fill_free_list()) == NULL) return 0; /* PyObject_New is inlined */ v = free_list; free_list = (PyIntObject *)v->ob_type; PyObject_INIT(v, &PyInt_Type); v->ob_ival = ival; small_ints[ival + NSMALLNEGINTS] = v; } #endif return 1; }
首先,正常状况下(排除内存不够),free* 相似命名的函数的返回值不会是NULL,因此直接忽略掉for循环中的if,在其下设一个断点观察free_list此时的值(被赋值以前或直接观察v的值),由于这是全局变量被赋值,记录一下它以前的值,说不定之后有用。
再往下看,除了PyObject_INIT函数(咱们先无论它,等HACK Python类型系统[f]的时候再研究),还有small_ints这个奇葩数组,根据名字,这是个在Python整型对象中必然会用到的东西,因此逃不掉了,不过还好,不就是个数组嘛!ui
咱们往上找这个small_ints数组的声明,看看他究竟暗藏了什么玄机。spa
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
发现了这一句,实在是太简单了,一个PyIntObject指针数组。大概长这个样子:
同时还发现了刚才不知道的宏,早就猜中的东西,如今是多少也可有可无了。但是这个small_ints究竟是用来干吗的还不清楚,仅仅知道它是什么永远很差玩儿,为何才是真正须要关注的。但是,怎么求出这个问题的答案呢?问源码做者最直接了,但是时效性太差,放弃;上网搜,太没挑战,放弃;还有源码,不知道可不能够,要回答的问题是为何,好比我为何须要一台电脑呢?回答是由于我在跑程序的时候要用。如今再来看一下_PyInt_Init对数组small_ints作了什么。
能够看到的是small_ints彻底是一个静态的结构,它是在_PyInt_Init被调用也就是系统初始化时就被直接分配了_intblock块,固然按照_intblock块的大小,N_INTOBJECTS为*((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject)),这是多少呢?还须要知道sizeof(PyIntObject) ,用gdb看看到这样:
因此一个_intblock能够容纳41个PyIntObject,比small_ints的size还小(因此下面的图有问题,不过这个信息不怎么重要,由于能够改small_ints的相关宏的值,让图变得正确)。反正在_PyInt_Init中,只要空间不够(free_list == NULL,if条件&&左值),就调用fill_free_list分配_intblock。按照默认的参数,大概得分配7个_intblock来完成_PyInt_Init(一样,由于要依靠参数,不重要)。
那如今,初始化过程已经完成了,咱们总结一下,_PyInt_Init的主要做用就是构建一个small_ints及其空间(在《Python源码剖析》用小整数池来描述,我以为这么多概念容易confuse,因此直接把本质说一下就好),但里面并无足够的信息来判断small_ints及其空间是如何被利用的,问题(为何须要small_ints?)依然没有被解决。_PyInt_Init这条线索虽然断了,但好在还有PyInt_FromLong。
注意到Python在这个时候已经经历了各类复杂的初始化过程,打印出了它的版本信息,万事俱备,只欠输入。不关注输入过程或者调用信息,假设如今就调用了PyInt_FromLong。
PyObject * PyInt_FromLong(long ival) { register PyIntObject *v; #if NSMALLNEGINTS + NSMALLPOSINTS > 0 if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { v = small_ints[ival + NSMALLNEGINTS]; Py_INCREF(v); #ifdef COUNT_ALLOCS if (ival >= 0) quick_int_allocs++; else quick_neg_int_allocs++; #endif return (PyObject *) v; } #endif if (free_list == NULL) { if ((free_list = fill_free_list()) == NULL) return NULL; } /* Inline PyObject_New */ v = free_list; free_list = (PyIntObject *)v->ob_type; PyObject_INIT(v, &PyInt_Type); v->ob_ival = ival; return (PyObject *) v; }
构造一个Python整数对象须要一个long型整数,若是这个long型整数大小是在-NSMALLNEGINTS到NSMALLPOSINTS之间,就认为它是一个小整数,在small_ints空间中找到封装该小整数的PyIntObject并调用Py_INCREF方法。这里经过命名能够知道Py_INCREF方法的做用是对对象的引用数作自增操做,具体实现不深刻。
固然上面只是针对小整数的状况,大整数是怎样处理的呢?继续往下看就能够知道。过程跟_PyInt_Init中同样,同样的经过判断条件语句的右值来调用fill_free_list方法。
其实大整数对象和小整数对象的区别就在于:
1. 小整数对象是在系统初始化的时候就为其分配了内存空间PyIntBlock(也就是 _intblock),并写入值,而对于大整数若是现有的以前分配好的PyIntBlock中有空间没用完的话就直接把值写入该块(固然写以前还要移动free_list并对对象作初始化操做),若是用完了就调用fill_free_list新建PyIntBlock。
2. 当要用一个小整数来构造小整数对象时,只对其相应的引用计数器作自增操做,而不像大整数那样作复杂的函数调用和内存分配操做,目的固然是时间效率,典型的那空间换时间的作法。
3. 本质上两者在内存中没有任何区别,小整数和大整数的界限能够看成参数来本身配置也能够说明这一点,不过这个界限究竟设为多少Python的效率能达到作好的平衡呢?不知道默认的参数设置成那样的缘由是什么,有没有更加科学的参数?
做为第一篇关于Hack Python的文章,里面有不少东西都比较啰嗦。要作的是还原整个探索的过程,包括全部走过的弯路,尤为要关注的是为何,而不只仅着眼因而什么。
对于Python类型系统的探索须要明确如下几点:
文章里面包含连接有碍于流畅阅读,因此取消文章内的连接,在末尾加参考资料部分以示引用或概念解释。
资料:
延伸:
【转载请注明出处 dukeyunz.com】