python - ticks - Warum ist[] schneller als list()?




python plot beschriftung (4)

Ich habe kürzlich die Verarbeitungsgeschwindigkeiten von [] und list() verglichen und festgestellt, dass [] mehr als dreimal schneller als list() . Ich habe den gleichen Test mit {} und dict() und die Ergebnisse waren praktisch identisch: [] und {} haben ungefähr 0,128 Sekunden / Million Zyklen gedauert, während list() und dict() ungefähr 0,428 Sekunden / Million Zyklen gedauert haben.

Warum ist das? Geben [] und {} (und wahrscheinlich () und '' auch) sofort Kopien eines leeren Aktien-Literal zurück, während die explizit genannten Gegenstücke ( list() , dict() , tuple() , str() ) Gehen Sie ganz daran, ein Objekt zu erstellen, ob sie tatsächlich Elemente haben oder nicht?

Ich habe keine Ahnung, wie sich diese beiden Methoden unterscheiden, aber ich würde es gerne herausfinden. Ich konnte in den Dokumenten oder auf SO keine Antwort finden, und die Suche nach leeren Klammern erwies sich als problematischer als erwartet.

Ich habe meine Timing-Ergebnisse durch Aufrufen von timeit.timeit("[]") und timeit.timeit("list()") und timeit.timeit("{}") und timeit.timeit("dict()") . Listen und Wörterbücher zu vergleichen. Ich verwende Python 2.7.9.

Ich habe kürzlich entdeckt, dass " Warum ist wenn wahr langsamer als wenn 1? ", Das die Leistung von if True zu if 1 und ein ähnliches wörtliches versus globales Szenario zu berühren scheint. vielleicht ist es auch eine Überlegung wert.


Warum ist [] schneller als list() ?

Der Hauptgrund dafür ist, dass Python list() wie eine benutzerdefinierte Funktion behandelt. list() bedeutet, dass Sie es abfangen können, indem Sie etwas anderes als eine list angeben und etwas anderes tun (z. B. eine eigene Unterklassenliste oder vielleicht eine Deque).

Mit [] sofort eine neue Instanz einer eingebauten Liste erstellt.

Meine Erklärung versucht, Ihnen die Intuition dafür zu geben.

Erläuterung

[] ist allgemein als wörtliche Syntax bekannt.

In der Grammatik wird dies als "Listendarstellung" bezeichnet. Aus den Dokumenten :

Eine Listenanzeige ist eine möglicherweise leere Reihe von Ausdrücken in eckigen Klammern:

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

Eine Listenanzeige liefert ein neues Listenobjekt, dessen Inhalt entweder durch eine Liste von Ausdrücken oder ein Verständnis angegeben wird. Wenn eine durch Kommas getrennte Liste von Ausdrücken angegeben wird, werden ihre Elemente von links nach rechts ausgewertet und in dieser Reihenfolge in das Listenobjekt eingefügt. Wenn ein Verständnis bereitgestellt wird, wird die Liste aus den Elementen erstellt, die sich aus dem Verständnis ergeben.

Kurz gesagt bedeutet dies, dass ein integriertes Objekt der list erstellt wird.

Dies lässt sich nicht umgehen - was bedeutet, dass Python dies so schnell wie möglich tun kann.

Andererseits kann list() mit dem eingebauten Listenkonstruktor daran gehindert werden, eine eingebaute list erstellen.

Nehmen wir zum Beispiel an, wir möchten, dass unsere Listen geräuschvoll erstellt werden:

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

Wir könnten dann die list im globalen Bereich auf Modulebene abfangen, und wenn wir dann eine list erstellen, erstellen wir tatsächlich unsere subtypisierte Liste:

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

Ebenso könnten wir es aus dem globalen Namespace entfernen

del list

und setze es in den eingebauten Namespace:

import builtins
builtins.list = List

Und nun:

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

Und beachten Sie, dass die Listenanzeige eine Liste bedingungslos erstellt:

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

Wir machen das wahrscheinlich nur vorübergehend, also machen wir unsere Änderungen rückgängig - entfernen Sie zuerst das neue List Objekt aus den Builtins:

>>> 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 nein, wir haben den Überblick über das Original verloren.

Keine Sorge, wir können immer noch eine list - es ist der Typ eines Listenliteral:

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

So...

Warum ist [] schneller als list() ?

Wie wir gesehen haben, können wir die list überschreiben, aber die Erstellung des Literal-Typs nicht abfangen. Wenn wir list wir nachsehen, ob etwas vorhanden ist.

Dann müssen wir anrufen, was auch immer anrufbar ist, nach dem wir gesucht haben. Aus der Grammatik:

Ein Aufruf ruft ein aufrufbares Objekt (zB eine Funktion) mit einer möglicherweise leeren Reihe von Argumenten auf:

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

Wir können sehen, dass es für jeden Namen dasselbe tut, nicht nur für die 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

Für [] gibt es keinen Funktionsaufruf auf der Python-Bytecode-Ebene:

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

Es geht einfach direkt zum Erstellen der Liste ohne Suchen oder Aufrufen auf Bytecode-Ebene.

Fazit

Wir haben gezeigt, dass eine list mithilfe der Gültigkeitsregeln mit Benutzercode abgefangen werden kann, und diese list() sucht nach einem aufrufbaren Wert und ruft ihn dann auf.

Während [] eine Listenanzeige oder ein Literal ist und somit die Namenssuche und den Funktionsaufruf vermeidet.


Die Antworten hier sind großartig und decken diese Frage vollständig ab. Ich werde den Byte-Code für die Interessenten noch weiter reduzieren. Ich verwende das neueste Repo von CPython. Ältere Versionen verhalten sich in dieser Hinsicht ähnlich, es können jedoch geringfügige Änderungen vorgenommen werden.

Hier ist eine BUILD_LIST der Ausführung für jeden dieser BUILD_LIST , BUILD_LIST für [] und CALL_FUNCTION für list() .

Die Anweisung BUILD_LIST :

Sie sollten nur den Horror sehen:

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();

Ich weiß, dass es furchtbar verwickelt ist. So einfach ist das:

  • Erstellen Sie mit PyList_New eine neue Liste (dies reserviert hauptsächlich den Speicher für ein neues oparg ), wobei oparg die Anzahl der Argumente auf dem Stapel oparg . Auf den Punkt.
  • Überprüfen Sie, if (list==NULL) mit if (list==NULL) nichts schief gelaufen ist.
  • Fügen Sie mit PyList_SET_ITEM (einem Makro) Argumente hinzu (in unserem Fall wird dies nicht ausgeführt), die sich auf dem Stapel PyList_SET_ITEM .

Kein Wunder, dass es schnell geht! Es ist maßgeschneidert für das Erstellen neuer Listen, sonst nichts :-)

Die Anweisung CALL_FUNCTION :

Das Erste, was Sie sehen, wenn Sie sich den Code CALL_FUNCTION der 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();

Sieht ziemlich harmlos aus, oder? Nun, nein, leider nicht, call_function ist kein direkter Typ, der die Funktion sofort call_function , es kann nicht. Stattdessen wird das Objekt aus dem Stapel abgerufen, alle Argumente des Stapels werden abgerufen, und anschließend wird basierend auf dem Objekttyp gewechselt. ist es ein:

Wir nennen den call_function , das an call_function PyList_Type Argument ist PyList_Type . CPython muss jetzt eine generische Funktion aufrufen, um alle aufrufbaren Objekte mit dem Namen _PyObject_FastCallKeywords zu verarbeiten.

Diese Funktion führt erneut einige Überprüfungen für bestimmte Funktionstypen durch (die ich nicht verstehen kann, warum) und ruft dann, nachdem bei Bedarf ein Diktat für kwargs erstellt wurde, _PyObject_FastCallDict .

_PyObject_FastCallDict bringt uns endlich irgendwohin! Nach weiteren Überprüfungen wird der Slot tp_call vom type des übergebenen type type.tp_call , type.tp_call . Anschließend wird aus den mit _PyStack_AsTuple Argumenten ein Tupel _PyStack_AsTuple und schließlich kann ein Aufruf erfolgen !

tp_call , das dem type.__call__ übernimmt und erstellt schließlich das type.__call__ . Es ruft die Listen __new__ die __new__ entsprechen, und reserviert Speicher für sie mit PyType_GenericAlloc : Dies ist tatsächlich der Teil, in dem es PyList_New endlich PyList_New . Alle vorherigen sind erforderlich, um Objekte generisch zu behandeln.

Am Ende ruft type_call list.__init__ und initialisiert die Liste mit allen verfügbaren Argumenten. Dann kehren wir auf dem Weg zurück, den wir gekommen sind. :-)

Erinnern Sie sich schließlich an LOAD_NAME , das ist ein weiterer Typ, der hier einen Beitrag leistet.

Es ist leicht einzusehen, dass Python bei der Bearbeitung unserer Eingaben im Allgemeinen durch die Rahmen springen muss, um tatsächlich die entsprechende C Funktion für die Ausführung der Aufgabe zu ermitteln. Es hat nicht den Vorteil, es sofort aufzurufen, da es dynamisch ist, jemand die list maskiert ( und ein Junge macht viele Leute ) und ein anderer Weg beschritten werden muss.

Hier verliert list() viel: Das erforschende Python muss herausfinden, was es tun soll.

Die wörtliche Syntax hingegen bedeutet genau eines; es kann nicht geändert werden und verhält sich immer vorbestimmt.

Fußnote: Alle Funktionsnamen können von einer Version zur anderen geändert werden. Der Punkt steht noch und wird höchstwahrscheinlich in zukünftigen Versionen stehen, es ist die dynamische Suche, die die Dinge verlangsamt.


Weil list eine function zum Konvertieren einer Zeichenfolge in ein Listenobjekt ist, während [] verwendet wird, um eine Liste zu erstellen. Versuchen Sie dies (könnte für Sie sinnvoller sein):

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

Während

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

Gibt Ihnen eine aktuelle Liste, die alles enthält, was Sie eingeben.


list() erfordert eine globale Suche und einen Funktionsaufruf, aber [] zu einer einzelnen Anweisung kompiliert. Sehen:

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




literals