CPython的动态运行时分发(与Java的静态编译时分发相比)只是Java比纯CPython更快的原因之一:Java中有jit编译,不同的垃圾回收策略,原生类型如int、double,而CPython中是不可变数据结构等等。早期的
表面实验已经表明,动态分发只占运行速度的约30% - 你不能用它来解释几个数量级的速度差异。为了让这个答案更具体化,让我们看一个例子:
def add(x,y):
return x+y
查看字节码:
import dis
dis.dis(add)
这将会给出:
2 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
在字节码级别上,无论x
和y
是整数、浮点数还是其他类型,解释器都不会关心。
在Java中情况完全不同:
int add(int x, int y) {return x+y;}
和
float add(float x, float y) {return x+y;}
这将导致完全不同的操作码,并且调用分派将在编译时发生 - 根据编译时已知的静态类型选择正确的版本。
很多时候,CPython解释器不必知道参数的确切类型:内部有一个基本的“类/接口”(显然在C中没有类,因此称为“协议”,但对于了解C++ / Java的人来说,“接口”可能是正确的心理模型),从中派生出所有其他“类”。这个基本“类”被称为PyObject
,这里是其协议的描述。。因此,只要函数是该协议/接口的一部分,CPython解释器就可以调用它,而无需知道确切的类型,调用将被分派到正确的实现(非常像C ++中的“虚拟”函数)。
在纯Python方面,似乎变量没有类型:
a=1
a="1"
然而,在内部,a
有一种类型 - 它是 PyObject*
,这个引用可以绑定到一个整数 (1
) 和一个 Unicode 字符串 ("1"
) - 因为它们都“继承”自 PyObject
。
时不时地,CPython 解释器会尝试找出引用的正确类型,对于上面的示例 - 当它看到 BINARY_ADD
操作码时,将执行 以下 C 代码:
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);
}
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"
或a=1
,使用不同的“类”- 但正如我们所看到的,这并不是唯一的地方。
因此,解释器在运行时干涉类型,但大多数时候它不必这样做-可以通过动态分派达到目标。