Динамическая диспетчеризация 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
целыми числами илиfloat или что-то еще - интерпретатору все равно.
Ситуация в Java совершенно другая:
int add(int x, int y) {return x+y;}
и
float add(float x, float y) {return x+y;}
wouЭто привело бы к совершенно разным кодам операций, и диспетчеризация вызова произошла бы во время компиляции - правильная версия выбирается в зависимости от статических типов, известных во время компиляции.
Довольно часто 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);
/* 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 ),otherwisРабота отправляется в PyNumber
-protocol.
Очевидно, что интерпретатор также должен знать точный тип при создании объекта, например, для a="1"
или a=1
различные "классы"используется - но, как мы видели, это не единственное место.
Таким образом, интерпретатор взаимодействует с типами во время выполнения, но большую часть времени ему не нужно это делать - цель можетбыть достигнутым посредством динамической отправки.