Перевод статьи подготовлен специально для студентов курса «Разработчик Python».
Когда вы пишете на низкоуровневом языке, таком как С, вы беспокоитесь о выборе правильного типа данных и спецификаторах для ваших целых чисел, на каждом шаге анализируете достаточно ли будет использовать просто
int
или нужно добавить long
или даже long double
. Однако при написании кода на Python вам не нужно беспокоиться об этих «незначительных» вещах, потому что Python может работать с числами типа integer
любого размера.
В С, если вы попытаетесь вычислить 220000 с помощью встроенной функции powl
, то на выходе получите inf
.
#include <stdio.h>
#include <math.h>
int main(void) {
printf("%Lfn", powl(2, 20000));
return 0;
}
$ ./a.out
inf
Но в Python сделать это проще простого:
>>> 2 ** 20000
39802768403379665923543072061912024537047727804924259387134 ...
...
... 6021 digits long ...
...
6309376
Должно быть под капотом Python делает что-то очень красивое и сегодня мы узнаем, что именно он делает, чтобы работать с целыми числами произвольного размера!
Представление и определение
Integer
в Python это структура C, определенная следующим образом:
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
PyObject_VAR_HEAD
– это макрос, он раскрывается в PyVarObject
, который имеет следующую структуру:
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
Другие типы, у которых есть PyObject_VAR_HEAD
:
- PyBytesObject
- PyTupleObject
- PyListObject
Это значит, что целое число, подобно кортежу или списку, имеет переменную длину, и это первый шаг к пониманию того, как Python может поддерживать работу с гигантскими числами. После раскрытия макроса _longobject
можно будет рассматривать как:
struct _longobject {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
digit ob_digit[1];
};
В структуре PyObject
есть некоторые мета-поля, используемые для подсчета ссылок (сборки мусора), но для того, чтобы поговорить об этом нужна отдельная статья. Поле, на котором мы сосредоточимся это ob_digit
и в немного ob_size
.
Расшифровка ob_digit
ob_digit
– это статически аллоцированый массив единичной длины типа digit (typedef для uint32_t)
. Поскольку это массив, ob_digit
в первую очередь является указателем на число, и, следовательно, при необходимости он может быть увеличен с помощью функции malloc до любой длины. Таким образом python может представлять и обрабатывать очень длинные числа.
Как правило в низкоуровневых языках, таких как С, точность целых чисел ограничена 64-битами, однако Python поддерживает целые числа произвольной точности. Начиная с версии Python 3, все числа представлены в виде bignum
и ограничены только доступной памятью системы.
Расшифровка ob_size
ob_size
хранит количество элементов в ob_digit
. Python переопределяет и затем использует значение ob_size
для определения фактического количества элементов, содержащихся в массиве, чтобы повысить эффективность выделения памяти массиву ob_digit
.
Хранение
Самый наивный способ хранить числа типа integer – это хранить одну десятичную цифру в одном элементе массива, тогда операции, такие как сложение и вычитание, могут выполняться по правилам математики из начальной школы.
С таким подходом число 5238 будет сохранено так:
Такой подход неэффективен, поскольку мы будем использовать до 32-бит цифр (uint32_t) для хранения десятичной цифры, которая, по сути, колеблется от 0 до 9 и может быть легко представлена всего лишь 4 битами, ведь при написании чего-то столь же универсального как python, разработчик ядра должен быть еще изобретательнее.
Итак, можем ли мы сделать лучше? Конечно, иначе я бы не выложил эту статью. Давайте подробнее рассмотрим то, как Python хранит сверхдлинное целое число.
Путь Python
Вместо того, чтобы хранить только одну десятичную цифру в каждом элементе массива ob_digit
, Python преобразует числа из системы счисления с основанием 10 в числа в системе с основанием 230 и вызывает каждый элемент, как цифру, значение которой колеблется от 0 до 230 — 1.
В шестнадцатеричной системе счисления, основание 16 ~ 24 означает, что каждая «цифра» шестнадцатеричного числа колеблется от 0 до 15 в десятичной системе счисления. В Python аналогично, «число» с основанием 230, что означает, что число будет варьироваться от 0 до 230 – 1 = 1073741823 в десятичной системе счисления.
Таким образом, Python эффективно использует почти все выделенное пространство в 32 бита на одну цифру, экономит ресурсы и все еще выполняет простые операции, такие как сложение и вычитание на уровне математики начальной школы.
В зависимости от платформы Python использует либо 32-битные целочисленные беззнаковые массивы, либо 16-битные целочисленные беззнаковые массивы с 15-битными цифрами. Для выполнения операций, которые будут рассмотрены дальше, понадобится всего несколько битов.
Пример: 1152921504606846976
Как уже упоминалось, для Python числа представлены в системе с основанием 230, то есть если вы конвертируете 1152921504606846976 в систему счисления с основанием 230, вы получите 100.
1152921504606846976 = 1 * (230)2 + 0 * (230)1 + 0 * (230)0
Поскольку ob_digit
первым хранит наименее значащую цифру, оно сохраняется как 001 в виде трех цифр.
Структура _longobject
для этого значения будет содержать:
ob_size
как 3ob_digit
как [0, 0, 1]
Я создал демонстрационный REPL, который покажет, как внутри себя Python хранит integer, а также ссылается на члены структуры, такие как ob_size
, ob_refcount
и т. д.
Операции над длинными числами типа integer
Теперь, когда у нас есть четкое представление о том, как Python реализует целые числа произвольной точности, настало время понять, как с ними выполняются различные математические операции.
Сложение
Целые числа хранятся «в цифрах», это означает, что сложение выполняется также просто, как в начальной школе, и исходный код Python показывает нам, что именно так сложение и реализовано. Функция с именем x_add
в файле longobject.c
выполняет сложение двух чисел.
...
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
for (; i < size_a; ++i) {
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
z->ob_digit[i] = carry;
...
Приведенный выше фрагмент кода взят из функции x_add
. Как видите, она перебирает число по цифрам и выполняет сложение цифр, вычисляет результат и добавляет перенос.
Интереснее становится, когда результатом сложения является отрицательное число. Знак ob_size
является знаком integer’а, то есть, если у вас есть отрицательное число, то ob_size
будет минусом. Значение ob_size
по модулю будет определять количество цифр в ob_digit
.
Вычитание
Подобно тому, как происходит сложение, происходит и вычитание. Функция с именем x_sub
в файле longobject.c
выполняет вычитание одного числа из другого.
...
for (i = 0; i < size_b; ++i) {
borrow = a->ob_digit[i] - b->ob_digit[i] - borrow;
z->ob_digit[i] = borrow & PyLong_MASK;
borrow >>= PyLong_SHIFT;
borrow &= 1; /* Keep only one sign bit */
}
for (; i < size_a; ++i) {
borrow = a->ob_digit[i] - borrow;
z->ob_digit[i] = borrow & PyLong_MASK;
borrow >>= PyLong_SHIFT;
borrow &= 1; /* Keep only one sign bit */
}
...
Приведенный выше фрагмент кода взят из функции x_sub
. В нем вы видите, как происходит перебор цифр и выполняется вычитание, вычисляется результат и распространяется перенос. Действительно, очень похоже на сложение.
Умножение
И снова умножение будет реализовано тем же наивным способом, который мы узнали из уроков математики в начальной школе, но он не отличается эффективностью. Чтобы поддерживать эффективность, Python реализует алгоритм Карацубы, который умножает два n-значных числа за O( nlog23) простых шагов.
Алгоритм непростой и его реализация выходит за рамки данной статьи, но вы можете найти его реализацию в функциях k_mul
и k_lopsided_mul
в файле longobject.c
.
Деление и другие операции
Все операции над целыми числами определены в файле longobject.c
, их очень просто найти и проследить работу каждой. Внимание: Детальное понимание работы каждой из них займет время, поэтому заранее запаситесь попкорном.
Оптимизация часто используемых целых чисел
Python заранее выделяет в памяти небольшое количество целых чисел в диапазоне от -5 до 256. Это выделение происходит во время инициализации, и поскольку мы не можем изменить целые числа (иммутабельность), эти предварительно выделенные числа являются синглтонами и на них ссылаются напрямую вместо реаллокации. Это значит, что каждый раз, когда мы используем/создаем маленькое число, Python вместо реаллокации просто возвращает ссылку на предварительно аллоцированное число.
Такую оптимизацию можно проследить в макросе IS_SMALL_INT
и функции get_small_int
в longobject.c
. Так Python экономит много места и времени на вычисление часто используемых чисел типа integer.
Это вторая статья из серии Python Internals. Первая статья была о том, как я изменил свою версию Python, сделав его двусмысленным. Она поможет вам сделать первые шаги в понимании исходного кода Python и продолжить путь к становлению разработчиком ядра Python.
Если вы хотите увидеть больше похожих статей, подпишитесь на мою рассылку и получайте их прямо в свой почтовый ящик. Я пишу об инженерии, системном проектировании и немного о программировании каждую пятницу. Пишите мне на @arpit_bhayani. Мои предыдущие статьи вы найдете на @arpitbhayani.me/blogs.
На этом все. До встречи на курсе!
Автор: MaxRokatansky