CPython的动态、运行时调度(与Java的静态编译时调度相比)只是Java比纯CPython快的原因之一:Java中有jit编译、不同的垃圾收集策略、本地类型(如int、double)与CPython中不可变数据结构的存在等等。在
我前面的superficial experiments已经表明,动态调度只负责大约30%的运行-你不能用这个来解释某些重要因素的速度差异。在
为了使这个答案不那么抽象,让我们看一个例子:def add(x,y):
return x+y
查看字节码:
^{pr2}$
它给出了:2 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
我们可以看到在字节码的级别上,x和{}是整数还是浮点数或其他东西没有区别-解释器不关心。在
Java的情况完全不同:int add(int x, int y) {return x+y;}
以及float add(float x, float y) {return x+y;}
将导致完全不同的操作码,并且调用调度将在编译时发生-根据编译时已知的静态类型选择正确的版本。在
通常的CPython解释器不必知道确切的参数类型:内部有一个基“类/接口”(显然C中没有类,所以它被称为“协议”,但是对于知道C++或java”接口的人来说,它可能是正确的心智模型,从中派生出所有其他类。这个基类称为PyObject和here is the description of its protocol.。只要函数是这个协议/接口的一部分,CPython解释器就可以调用它,而不知道确切的类型,并且调用将被分派给正确的实现(很多类似于C++中的“虚”函数)。在
在纯Python方面,似乎变量没有类型:a=1
a="1"
但是,在内部,a有一个类型,它是PyObject*,这个引用可以绑定到一个整数(1)和一个unicode字符串("1"),因为它们都是从PyObject中“继承”的。在
CPython解释器不时地试图找出引用的正确类型,同样对于上面的例子-当它看到BINARY_ADD-opcode时,following C-code将被执行:case TARGET(BINARY_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
...
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
在这里,解释器查询两个对象是否都是unicode字符串,如果是这种情况,则使用一个特殊的方法(可能更有效,因为它试图在适当的地方更改不可变的unicode对象,请参见这个SO-answer),否则工作被分派到PyNumber-协议。在
显然,解释器还必须知道创建对象时的确切类型,例如,a="1"或{}使用了不同的“类”,但正如我们所看到的,它不是唯一的地方。在
因此,解释器在运行时转换类型,但大多数情况下它不必这么做-目标可以通过动态调度来实现。在