web-dev-qa-db-fra.com

Pourquoi les tableaux de Python sont-ils lents?

J'esperais array.array être plus rapide que les listes, car les tableaux semblent sans boîte.

Cependant, j'obtiens le résultat suivant:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Quelle pourrait être la cause d'une telle différence?

146
Valentin Lorentz

Le stockage est "décompressé", mais chaque fois que vous accédez à un élément, Python doit "le" boxer "(l'intégrer dans un objet Python normal) dans afin de faire quoi que ce soit avec elle. Par exemple, votre sum(A) effectue une itération sur le tableau et encadre chaque entier, un à la fois, dans un objet Python int normal. Cela coûte du temps. Dans votre sum(L), toute la boxe a été faite au moment de la création de la liste.

Donc, au final, un tableau est généralement plus lent, mais nécessite beaucoup moins de mémoire.


Voici le code pertinent d'une version récente de Python 3, mais les mêmes idées de base s'appliquent à toutes les implémentations CPython depuis la sortie de Python.

Voici le code pour accéder à un élément de la liste:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Il y a très peu de choses à faire: somelist[i] Renvoie simplement le i ème objet de la liste (et tous les objets Python de CPython sont des pointeurs sur une structure dont le segment initial est conforme la mise en page d'un struct PyObject).

Et voici l'implémentation __getitem__ Pour un array avec le code de type l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

La mémoire brute est traitée comme un vecteur d’entiers au format natif Clong; le i 'th C long est lu; puis PyLong_FromLong() est appelé pour envelopper ("boîte") le C long natif dans un objet Python long (qui, dans Python 3, qui élimine la distinction de Python 2 entre int et long, est en fait affiché sous le type int).

Cette boxe doit allouer une nouvelle mémoire pour un objet Python int, et y pulvériser les bits natifs C long. Dans le contexte de l'exemple d'origine, la durée de vie de cet objet est très brève (juste le temps que sum() ajoute le contenu à un total cumulé), puis il faut plus de temps pour désallouer le nouveau int objet.

C’est de là que vient la différence de vitesse, a toujours été et viendra toujours dans l’implémentation CPython.

210
Tim Peters

Pour compléter l'excellente réponse de Tim Peters, les tableaux implémentent le protocole de tampon , contrairement aux listes. Cela signifie que, si vous écrivez une extension C (ou l’équivalent moral, comme écrire un module Cython ), vous pouvez alors accéder aux éléments d’un tableau et les utiliser beaucoup plus rapidement que tout ce que Python peut faire. Cela vous donnera des améliorations de vitesse considérables, peut-être bien supérieures à un ordre de grandeur. Cependant, il présente un certain nombre d'inconvénients:

  1. Vous écrivez maintenant C au lieu de Python. Cython est un moyen d’améliorer cela, mais il n’élimine pas beaucoup de différences fondamentales entre les langages; vous devez connaître la sémantique C et comprendre ce qu’elle fait.
  2. L'API C de PyPy fonctionne dans une certaine mesure , mais n'est pas très rapide. Si vous ciblez PyPy, vous devriez probablement simplement écrire du code simple avec des listes régulières, puis laisser le JITter l’optimiser pour vous.
  3. Les extensions C sont plus difficiles à distribuer que le code pur Python car elles doivent être compilées. La compilation dépend généralement de l'architecture et du système d'exploitation. Vous devez donc vous assurer que vous compilez pour votre plate-forme cible.

Aller directement aux extensions C peut utiliser une massue pour balayer une mouche, selon votre cas d'utilisation. Vous devriez d’abord examiner NumPy et voir s’il est assez puissant pour faire tout ce que vous essayez de faire. Il sera également beaucoup plus rapide que le Python natif, si utilisé correctement.

82
Kevin

Tim Peters a répondu pourquoi c'est lent, mais voyons comment l'améliorer.

S'en tenir à votre exemple de sum(range(...)) (facteur 10 plus petit que votre exemple à mémoriser ici):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

De cette façon, numpy doit aussi box/unbox, ce qui entraîne une surcharge supplémentaire. Pour faire vite, il faut rester dans le code numpy c:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Donc, de la solution de liste à la version numpy, il s'agit d'un facteur 16 au moment de l'exécution.

Voyons aussi combien de temps il faut pour créer ces structures de données

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Gagnant clair: Numpy

Notez également que la création de la structure de données prend environ autant de temps que la somme, voire davantage. L'allocation de mémoire est lente.

Utilisation de la mémoire de ceux-ci:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Donc, ils prennent 8 octets par nombre avec une surcharge variable. Pour la gamme, nous utilisons des ints de 32 bits suffisent, nous pouvons donc sécuriser un peu de mémoire.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Mais il s’avère que l’ajout d’ints 64 bits est plus rapide que celui sur ma machine, cela ne vaut donc le coup que si vous êtes limité par la mémoire/la bande passante.

8
Robin Roth