Bagaimana tipe integer yang sangat panjang diimplementasikan dalam Python?

Terjemahan artikel disiapkan khusus untuk siswa kursus Pengembang Python .





Saat Anda menulis dalam bahasa tingkat rendah seperti C, Anda khawatir tentang memilih tipe data dan kualifikasi yang tepat untuk bilangan bulat Anda, pada setiap langkah Anda menganalisis apakah akan cukup untuk menggunakannya secara sederhana intatau apakah Anda perlu menambahkan longatau bahkan long double. Namun, saat menulis kode dengan Python, Anda tidak perlu khawatir tentang hal-hal "minor" ini, karena Python dapat bekerja dengan angka dari integersemua jenis ukuran.

Di C, jika Anda mencoba menghitung 2 20000 menggunakan fungsi bawaan powl, Anda akan mendapatkan hasilnya inf.

#include <stdio.h>
#include <math.h>

int main(void) {
  printf("%Lf\n", powl(2, 20000));
  return 0;
}

$ ./a.out
inf

Namun dengan Python, menjadikan ini lebih mudah dari sebelumnya adalah mudah:

>>> 2 ** 20000
39802768403379665923543072061912024537047727804924259387134 ...
...
... 6021 digits long ...
...
6309376

Pasti di bawah kap bahwa Python melakukan sesuatu yang sangat indah, dan hari ini kita akan mencari tahu apa fungsinya untuk bekerja dengan bilangan bulat ukuran sewenang-wenang!

Presentasi dan Definisi


Integer dalam Python, ini adalah struktur C yang didefinisikan sebagai berikut:

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

PyObject_VAR_HEADMerupakan makro, yang diperluas PyVarObject, yang memiliki struktur berikut:

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

Jenis lain yang memiliki PyObject_VAR_HEAD:

  • PyBytesObject
  • PyTupleObject
  • PyListObject

Ini berarti bahwa integer, seperti tuple atau daftar, memiliki panjang variabel, dan ini adalah langkah pertama untuk memahami bagaimana Python dapat mendukung kerja dengan angka raksasa. Setelah diperluas, makro _longobjectdapat dianggap sebagai:

struct _longobject {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
    digit ob_digit[1];
};

Ada PyObjectbeberapa bidang meta dalam struktur yang digunakan untuk penghitungan referensi (pengumpulan sampah), tetapi untuk membicarakan hal ini, kita memerlukan artikel terpisah. Bidang yang akan kita fokuskan ini ob_digitdan sedikit lagi ob_size.

Dekripsi ob_digit


ob_digitMerupakan array yang dialokasikan secara statis dari jenis satuan panjang digit (typedef uint32_t). Karena ini adalah array, ob_digitini terutama adalah penunjuk ke angka, dan karena itu, jika perlu, dapat ditingkatkan menggunakan fungsi malloc dengan panjang apa pun. Dengan cara ini python dapat mewakili dan memproses angka yang sangat panjang.

Biasanya, dalam bahasa tingkat rendah seperti C, ketepatan bilangan bulat dibatasi hingga 64 bit, namun Python mendukung bilangan bulat dengan ketepatan acak . Dimulai dengan Python 3, semua angka disajikan dalam bentuk bignumdan hanya dibatasi oleh memori sistem yang tersedia.

Dekripsi ob_size


ob_sizemenyimpan jumlah item dalam ob_digit. Python menimpa dan kemudian menggunakan nilai ob_sizeuntuk menentukan jumlah elemen yang terkandung dalam array untuk meningkatkan efisiensi mengalokasikan memori ke array ob_digit.

Penyimpanan


Cara yang paling naif untuk menyimpan bilangan bulat adalah dengan menyimpan satu angka desimal dalam satu elemen array, maka operasi seperti penjumlahan dan pengurangan dapat dilakukan sesuai dengan aturan matematika dari sekolah dasar.

Dengan pendekatan ini, angka 5238 akan disimpan seperti ini:



Pendekatan ini tidak efisien, karena kita akan menggunakan hingga 32-bit digit (uint32_t) untuk menyimpan angka desimal, yang, pada kenyataannya, berkisar dari 0 hingga 9 dan dapat dengan mudah diwakili dengan hanya 4 bit, karena ketika menulis sesuatu yang serbaguna seperti python, pengembang kernel harus lebih inventif.

Jadi, bisakah kita melakukan yang lebih baik? Tentu saja, kalau tidak saya tidak akan memposting artikel ini. Mari kita lihat lebih dekat bagaimana Python menyimpan integer ekstra panjang.

Jalur python


Alih-alih menyimpan hanya satu angka desimal di setiap elemen array ob_digit, Python mengubah angka dari sistem angka dengan basis 10 ke angka dalam sistem dengan basis 2 30 dan memanggil setiap elemen sebagai digit yang nilainya berkisar dari 0 hingga 2 30 - 1.

dalam sistem angka heksadesimal, basis 16 ~ 2 4 berarti bahwa setiap "digit" dari rentang angka heksadesimal 0-15 dalam sistem angka desimal. Dalam Python, demikian pula, "angka" dengan basis 2 30 , yang berarti bahwa jumlahnya akan bervariasi dari 0 hingga 2 30 - 1 = 1073741823 dalam desimal.

Dengan demikian, Python efektif menggunakan hampir semua ruang yang dialokasikan dari 32 bit per digit, menghemat sumber daya, dan masih melakukan operasi sederhana seperti penambahan dan pengurangan pada tingkat matematika sekolah dasar.

Bergantung pada platform, Python menggunakan array integer 32-bit unsigned atau array integer 16-bit dengan digit 15-bit. Untuk melakukan operasi yang akan dibahas nanti, Anda hanya perlu beberapa bit.

Contoh: 1152921504606846976

Seperti yang telah disebutkan, untuk Python, angka diwakili dalam sistem dengan basis 2 30 , yaitu, jika Anda mengubah 1152921504606846976 menjadi sistem angka dengan basis 2 30 , Anda akan mendapatkan 100.

1152921504606846976 = 1 * (2 30) 2 + 0 * (2 30 ) 1 + 0 * (2 30 ) 0

Karena ini adalah yang ob_digitpertama menyimpan digit paling tidak signifikan, disimpan sebagai 001 dalam bentuk tiga digit.

Struktur _longobjectuntuk nilai ini akan mengandung:

  • ob_size seperti 3
  • ob_digit seperti [0, 0, 1]



Saya membuat demo REPL yang akan menunjukkan bagaimana Python menyimpan integer di dalamnya, dan juga merujuk pada anggota struktur seperti ob_size, ob_refcountdll.

Operasi Panjang Integer


Sekarang kita memiliki gagasan yang jelas tentang bagaimana Python mengimplementasikan bilangan bulat dari presisi sewenang-wenang, sekarang saatnya untuk memahami bagaimana berbagai operasi matematika dilakukan dengan mereka.

Tambahan


Integer disimpan "dalam angka", yang berarti bahwa penambahan itu sesederhana di sekolah dasar, dan kode sumber Python menunjukkan kepada kita bahwa ini adalah bagaimana penambahan diterapkan. Fungsi dengan nama x_adddalam file longobject.cmenambah dua angka.

...
    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;
...

Cuplikan kode di atas diambil dari suatu fungsi x_add. Seperti yang Anda lihat, itu berulang atas angka dengan angka dan melakukan penambahan angka, menghitung hasilnya dan menambahkan tanda hubung.

Menjadi lebih menarik ketika hasil penambahan adalah angka negatif. Tanda ob_sizeadalah tanda bilangan bulat, yaitu, jika Anda memiliki angka negatif, maka itu ob_sizeakan menjadi minus. ob_sizeModulo nilai akan menentukan jumlah digit dalam ob_digit.

Pengurangan


Sama seperti penambahan terjadi, pengurangan juga terjadi. Fungsi dengan nama x_subdalam file longobject.cmengurangi satu nomor dari yang lain.

...
    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 */
    }
...

Cuplikan kode di atas diambil dari suatu fungsi x_sub. Di dalamnya, Anda melihat bagaimana penghitungan angka terjadi dan pengurangan dilakukan, hasilnya dihitung dan transfer didistribusikan. Memang, ini sangat mirip dengan penambahan.

Perkalian


Dan lagi, perkalian akan dilaksanakan dengan cara yang sama naif yang kita pelajari dari pelajaran matematika di sekolah dasar, tetapi itu tidak terlalu efisien. Untuk menjaga efisiensi, Python mengimplementasikan algoritma Karatsuba , yang mengalikan dua angka n-digit dalam langkah-langkah sederhana O (n log 2 3 ).

Algoritma ini tidak sederhana dan implementasinya berada di luar cakupan artikel ini, tetapi Anda dapat menemukan implementasinya dalam fungsi k_mul dan k_lopsided_mulfile longobject.c.

Divisi dan operasi lainnya


Semua operasi pada bilangan bulat didefinisikan dalam file longobject.c, mereka sangat sederhana untuk menemukan dan melacak pekerjaan masing-masing. Perhatian: Pemahaman terperinci tentang pekerjaan masing-masing dari mereka akan memakan waktu, jadi siapkan dengan popcorn .


Python melakukan pra-alokasi sejumlah kecil bilangan bulat dalam memori mulai dari -5 hingga 256. Alokasi ini terjadi selama inisialisasi, dan karena kami tidak dapat mengubah bilangan bulat (kekekalan), angka-angka yang dialokasikan sebelumnya adalah singleton dan direferensikan langsung alih-alih dialokasikan. Ini berarti bahwa setiap kali kita menggunakan / membuat angka kecil, Python alih-alih menjualnya hanya mengembalikan referensi ke nomor yang dialokasikan sebelumnya.

Optimalisasi tersebut dapat dilacak di makro IS_SMALL_INTdan fungsi get_small_intc longobject.c. Jadi Python menghemat banyak ruang dan waktu dalam menghitung angka integer yang umum digunakan.

Ini adalah artikel kedua dalam seri Python Internals. Artikel pertama adalah tentang bagaimana saya mengubah versi Python saya untuk membuatnyaambigu . Ini akan membantu Anda mengambil langkah pertama dalam memahami kode sumber Python dan melanjutkan jalur untuk menjadi pengembang kernel Python.

Jika Anda ingin melihat lebih banyak artikel serupa, berlangganan buletin saya dan terima langsung di kotak masuk Anda. Saya menulis tentang teknik, sistem rekayasa dan sedikit tentang pemrograman setiap hari Jumat. Email saya di @arpit_bhayani . Anda dapat menemukan artikel saya sebelumnya di @ arpitbhayani.me / blog .

Itu saja. Sampai jumpa di lapangan !

All Articles