Notes on CPython List Internals

În timp ce învățam să programez, listele Python mi se păreau cu totul magice. Mi le imaginam ca fiind implementate de un fel de structură de date magică care era în parte listă legată, în parte matrice, care era perfectă pentru orice.

Cum am crescut ca inginer, am realizat că acest lucru era puțin probabil. Am presupus (corect) că, mai degrabă decât un fel de implementare magică, era pur și simplu susținută de o matrice redimensionabilă. Am decis să citesc codul și să aflu.

Unul dintre lucrurile frumoase despre CPython este implementarea lizibilă. Deși fișierul relevant are peste 2000 de linii de C, este vorba în principal de algoritmul de sortare și de boilerplate pentru a face funcțiile apelabile din codul Python.1 Operațiile de bază ale listei sunt scurte și directe.

Iată câteva lucruri interesante pe care le-am găsit citind implementarea. Fragmentele de cod de mai jos provin din sursa CPython, cu comentarii explicative adăugate de mine.

List Resizing

Dacă adăugați la o listă Python și array-ul suport nu este suficient de mare, array-ul suport trebuie extins. Când se întâmplă acest lucru, matricea de suport este mărită cu aproximativ 12%. Personal, am presupus că acest factor de creștere este mult mai mare. În Java, ArrayList crește cu 50% atunci când este extins2, iar în Ruby, Array crește cu 100%.3

// essentially, the new_allocated = new_size + new_size / 8new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

link to CPython reallocation code

Am făcut câteva experimente de performanță – prealocarea array-urilor cu construcții precum *500 nu pare să facă nicio diferență notabilă. În testul meu de referință neștiințific de adăugare la o listă de 100 de milioane de ori, Python 3 a fost mult mai lent decât Python 2, care a fost mult mai lent decât Ruby, cu toate acestea, sunt necesare mult mai multe cercetări pentru a determina impactul (dacă există) al factorului de creștere asupra performanței de inserție.

Inserție la începutul listei

Inserția la începutul unei liste necesită timp liniar – acest lucru nu este atât de surprinzător având în vedere restul implementării, dar este bine de știut că some_list.insert(0,value) este rareori o idee bună. Un utilizator de pe Reddit mi-a amintit de Deques, care negociază inserarea și eliminarea în timp constant de la ambele capete în schimbul indexării în timp constant.

// First, shift all the values after our insertion point// over by onefor (i = n; --i >= where; ) items = items;// Increment the number of references to v (the value we're inserting)// for garbage collectionPy_INCREF(v);// insert our actual itemitems = v;return 0;

link to CPython insertion code

Crearea de felii de liste

Prinderea unei felii dintr-o listă, de ex. some_listeste, de asemenea, o operațiune în timp liniar în mărimea feliei, deci, din nou, fără magie. V-ați putea imagina optimizarea acestui lucru cu un fel de semantică de copiere la scriere, dar codul CPython favorizează simplitatea:

for (i = 0; i < len; i++) { PyObject *v = src; Py_INCREF(v); dest = v;}

link to CPython slice code

Slice Assignment

Puteți atribui o felie! Sunt sigur că acest lucru este cunoscut în mod obișnuit printre dezvoltatorii Python profesioniști, dar eu nu m-am întâlnit niciodată cu el în câțiva ani de programare python. am descoperit doar când am dat peste funcția list_ass_slice(...) din cod. Cu toate acestea, atenție la listele mari – trebuie să copieze toate elementele șterse din lista originală, ceea ce va dubla pentru scurt timp utilizarea memoriei.

>>> a = >>> a = 'a'>>> a>>> a = >>> a = >>> a

Sortare

Array-urile Python sunt sortate cu un algoritm cunoscut sub numele de “timsort”. Este extrem de complicat și este descris în detaliu într-un document secundar din arborele de surse. În linii mari, acesta construiește serii din ce în ce mai lungi de elemente secvențiale și le unește. Spre deosebire de sortarea prin îmbinare normală, acesta începe prin a căuta secțiuni ale listei care sunt deja sortate (runs în cod). Acest lucru îi permite să profite de datele de intrare care sunt deja parțial sortate.O informație interesantă: Pentru sortarea micilor array-uri (sau a micilor secțiuni ale unui array mai mare) de până la 64 de elemente4, timsort utilizează “sortarea binară”. Aceasta este, în esență, sortarea prin inserție, dar folosind căutarea binară pentru a introduce elementul în locația corectă. Este, de fapt, un algoritm O(n^2)! Un exemplu interesant de performanță în lumea reală care învinge în practică complexitatea algoritmică. link

Am pierdut ceva grozav la listele CPython? Anunțați-mă în comentarii.

Mulțumesc lui Leah Alpert pentru că a oferit sugestii pentru această postare.

Vreți să primiți un e-mail despre noile postări de pe blog?

Postăm cam o dată la câteva săptămâni pe subiecte ca bazele de date, limbajele interne și algoritmii și, recent, deep learning.Vreți să mă angajați? Sunt disponibil pentru angajamente de la 1 săptămână la câteva luni. Angajează-mă!

  1. Am dat peste codul de generare a boilerplate-ului în timp ce scriam acest post și este super tare! Un preprocesor C bazat pe python generează și întreține macrocomenzi pentru a face parsarea argumentelor și munging între Python și C
  2. Open JDK 6: int newCapacity = (oldCapacity * 3)/2 + 1; Open JDK 8 int newCapacity = oldCapacity + (oldCapacity >> 1);
  3. https://github.com/ruby/ruby/blob/0d09ee1/array.c#L392
  4. https://github.com/python/cpython/blob/1fb72d2ad243c965d4432b4e93884064001a2607/Objects/listobject.c#L1923

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.