web-dev-qa-db-fra.com

Pourquoi [] est-il plus rapide que list ()?

J'ai récemment comparé les vitesses de traitement de [] et de list() et j'ai été surpris de découvrir que [] s'exécute plus de trois fois plus rapidement que list(). J'ai exécuté le même test avec {} et dict() et les résultats étaient pratiquement identiques: [] et {} prenaient chacun environ 0,128sec/million de cycles, alors que list() et dict() prenaient environ 0,428sec/million cycles chacun.

Pourquoi est-ce? Est-ce que [] et {} (et probablement () et '', aussi) renvoient immédiatement une copie d'un littéral de stock vide tandis que leurs homologues nommés explicitement (list(), dict(), Tuple(), str()) crée-t-il pleinement un objet, qu’il ait ou non des éléments?

Je n'ai aucune idée de la façon dont ces deux méthodes diffèrent, mais j'aimerais le savoir. Je ne pouvais pas trouver de réponse dans la documentation ou sur le SO, et la recherche de crochets vides s'est avérée plus problématique que prévu.

J'ai obtenu les résultats de mon chronométrage en appelant timeit.timeit("[]") et timeit.timeit("list()"), et timeit.timeit("{}") et timeit.timeit("dict()"), pour comparer les listes et les dictionnaires, respectivement. J'utilise Python 2.7.9.

J'ai récemment découvert " Pourquoi est-ce que si True est plus lent que si 1? " qui compare les performances de if True à if 1 et semble toucher à un scénario similaire littéral/global ; Peut-être que cela vaut également la peine d'être examiné.

666
Augusta

Parce que [] et {} sont syntaxe littérale . Python peut créer du bytecode uniquement pour créer la liste ou les objets du dictionnaire:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list() et dict() sont des objets distincts. Leurs noms doivent être résolus, la pile doit être impliquée pour pousser les arguments, la trame doit être stockée pour une récupération ultérieure et un appel doit être effectué. Cela prend plus de temps.

Pour le cas vide, cela signifie que vous avez au minimum un LOAD_NAME (qui doit rechercher dans l’espace de nom global ainsi que dans le module __builtin__ ) suivi d'un CALL_FUNCTION , qui doit conserver le cadre actuel:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

Vous pouvez programmer la recherche de nom séparément avec timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

La différence de temps il y a probablement une collision de dictionnaire de hachage. Soustrayez ces heures des heures d'appel de ces objets et comparez le résultat avec les heures d'utilisation des littéraux:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

Il faut donc 1.00 - 0.31 - 0.30 == 0.39 secondes supplémentaires par tranche de 10 millions d'appels pour appeler l'objet.

Vous pouvez éviter le coût global de la recherche en créant un alias dans les noms globaux (en utilisant une configuration timeit, tout ce que vous associez à un nom est local):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

mais vous ne pouvez jamais surmonter ce coût CALL_FUNCTION.

718
Martijn Pieters

list() nécessite une recherche globale et un appel de fonction, mais [] est compilé en une seule instruction. Voir:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None
141
Dan D.

Parce que list est un fonction pour convertir, disons une chaîne en un objet de liste, alors que [] est utilisé pour créer une liste à la volée. Essayez ceci (peut-être plus logique pour vous):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

Tandis que

y = ["wham bam"]
>>> y
["wham bam"]

Vous donne une liste réelle contenant tout ce que vous y mettez.

74
Torxed

Les réponses ici sont excellentes, pertinentes et couvrent pleinement cette question. Je laisserai tomber un peu plus loin le code octet pour ceux qui sont intéressés. J'utilise le dernier repo de CPython; Les anciennes versions se comportent de manière similaire à cet égard, mais de légères modifications pourraient être apportées.

Voici une ventilation de l'exécution pour chacun de ces éléments, _BUILD_LIST_ pour _[]_ et _CALL_FUNCTION_ pour list().


L'instruction _BUILD_LIST_:

Vous devriez juste voir l'horreur:

_PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
Push(list);
DISPATCH();
_

Terriblement compliqué, je sais. Voici à quel point c'est simple:

  • Créez une nouvelle liste avec PyList_New (cela alloue principalement de la mémoire pour un nouvel objet liste), oparg indiquant le nombre d'arguments de la pile. Droit au but.
  • Vérifiez que rien ne s'est mal passé avec if (list==NULL).
  • Ajoutez tous les arguments (dans notre cas ce n’est pas exécuté) situés dans la pile avec PyList_SET_ITEM (une macro).

Pas étonnant que c'est rapide! C'est fait sur mesure pour créer de nouvelles listes, rien d'autre :-)

L'instruction _CALL_FUNCTION_:

Voici la première chose que vous voyez lorsque vous jetez un œil à la gestion du code _CALL_FUNCTION_:

_PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
Push(res);
if (res == NULL) {
    goto error;
}
DISPATCH();
_

Semble assez inoffensif, non? Eh bien, non, malheureusement pas, call_function n'est pas un gars simple qui appelle la fonction immédiatement, il ne peut pas. Au lieu de cela, il récupère l'objet de la pile, tous les arguments de la pile, puis bascule en fonction du type de l'objet; est-ce un:

Nous appelons le type list, l'argument transmis à _call_function_ est PyList_Type . CPython doit maintenant appeler une fonction générique pour gérer tous les objets appelables nommés _PyObject_FastCallKeywords , voire davantage d’appels de fonction.

Cette fonction vérifie à nouveau certains types de fonctions (ce que je ne comprends pas pourquoi), puis, après avoir créé un dict pour kwargs si nécessaire , passe à appel _PyObject_FastCallDict .

__PyObject_FastCallDict_ nous emmène enfin quelque part! Après avoir effectué encore plus de vérifications , il saisit le créneau _tp_call_ du type du type que nous avons transmis c’est-à-dire qu’il saisit _type.tp_call_. Il crée ensuite un tuple à partir des arguments passés avec __PyStack_AsTuple_ et, enfin, n appel peut finalement être passé!

_tp_call_, qui correspond à type.__call__ prend le relais et crée finalement l'objet liste. Il appelle les listes ___new___ qui correspond à PyType_GenericNew et lui alloue de la mémoire avec PyType_GenericAlloc : C'est en fait la partie où il rattrape _PyList_New_, enfin . Tous les précédents sont nécessaires pour manipuler les objets de manière générique.

En fin de compte, _type_call_ appelle _list.__init___ et initialise la liste avec tous les arguments disponibles, puis nous retournons dans la voie empruntée. :-)

Enfin, rappelez-vous le _LOAD_NAME_, c’est un autre gars qui contribue ici.


Il est facile de voir que, lorsque nous traitons avec notre entrée, Python doit généralement sauter entre des cerceaux afin de déterminer la fonction C appropriée pour effectuer le travail. Il n'a pas la courtoisie de l'appeler immédiatement car il est dynamique, quelqu'un peut masquer list ( et le garçon le fait souvent ) et un autre chemin doit être pris.

C’est là que list() perd beaucoup: L’exploration Python doit _ faire pour savoir ce qu’elle devrait faire.

La syntaxe littérale, d'autre part, signifie exactement une chose; il ne peut pas être changé et se comporte toujours de manière prédéterminée.

Note de bas de page: Tous les noms de fonction sont susceptibles de changer d’une publication à l’autre. Le point est toujours valable et le sera très probablement dans les versions futures, c’est la recherche dynamique qui ralentit les choses.

18

Pourquoi [] est-il plus rapide que list()?

La principale raison est que Python traite list() comme une fonction définie par l'utilisateur, ce qui signifie que vous pouvez l'intercepter en aliasant quelque chose d'autre en list et en faisant quelque chose de différent (comme utiliser votre propre liste de sous-classes ou peut-être une deque).

Il crée immédiatement une nouvelle instance d'une liste intégrée avec [].

Mon explication cherche à vous en donner l'intuition.

Explication

[] est communément appelé syntaxe littérale.

Dans la grammaire, cela s'appelle un "affichage de liste". de la documentation :

Un affichage sous forme de liste est une série d’expressions éventuellement vides placées entre crochets:

list_display ::=  "[" [starred_list | comprehension] "]"

Un affichage de liste génère un nouvel objet de liste, le contenu étant spécifié soit par une liste d'expressions, soit par une compréhension. Lorsqu'une liste d'expressions séparées par des virgules est fournie, ses éléments sont évalués de gauche à droite et placés dans l'objet de liste dans cet ordre. Lorsqu'une compréhension est fournie, la liste est construite à partir des éléments résultant de la compréhension.

En bref, cela signifie qu'un objet intégré de type list est créé.

Il n’est pas possible de contourner cela - ce qui signifie que Python peut le faire aussi rapidement que possible.

D'autre part, list() peut être intercepté en créant un list intégré à l'aide du constructeur de liste intégré.

Par exemple, supposons que nous voulions que nos listes soient créées bruyamment:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

Nous pourrions alors intercepter le nom list sur la portée globale du module, puis créer un list pour créer notre liste de sous-types:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

De même, nous pourrions le supprimer de l'espace de noms global

del list

et le mettre dans le namespace intégré:

import builtins
builtins.list = List

Et maintenant:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

Et notez que l’affichage de la liste crée inconditionnellement une liste:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

Nous ne le faisons probablement que temporairement, alors annulons nos modifications - supprimons d’abord le nouvel objet List des commandes intégrées:

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

Oh non, nous avons perdu la trace de l'original.

Ne vous inquiétez pas, nous pouvons toujours obtenir list - c'est le type d'un littéral de liste:

>>> builtins.list = type([])
>>> list()
[]

Alors...

Pourquoi [] est-il plus rapide que list()?

Comme nous l'avons vu - nous pouvons écraser list - mais nous ne pouvons pas intercepter la création du type littéral. Lorsque nous utilisons list, nous devons faire des recherches pour voir s’il ya quelque chose.

Ensuite, nous devons appeler le callable que nous avons levé. De la grammaire:

Un appel appelle un objet appelable (une fonction, par exemple) avec une série d'arguments éventuellement vide:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

Nous pouvons voir qu'il fait la même chose pour n'importe quel nom, pas seulement la liste:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

Pour [], il n'y a pas d'appel de fonction au niveau du Python _:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

Il va tout simplement directement à la construction de la liste sans aucune recherche ou appel au niveau du bytecode.

Conclusion

Nous avons démontré que list peut être intercepté avec un code utilisateur à l'aide des règles de portée, et que list() cherche un appelable, puis l'appelle.

Alors que [] est un affichage sous forme de liste, ou littéral, et évite ainsi la recherche de nom et l'appel de fonction.

11
Aaron Hall