Was ist das schnellste (zugreifbare) strukturähnliche Objekt in Python?

77

Ich optimiere Code, dessen Hauptengpass darin besteht, auf eine sehr große Liste strukturähnlicher Objekte zuzugreifen. Derzeit verwende ich Namedtuples, um die Lesbarkeit zu verbessern. Ein schnelles Benchmarking mit 'timeit' zeigt jedoch, dass dies wirklich der falsche Weg ist, wenn Leistung ein Faktor ist:

Benanntes Tupel mit a, b, c:

>>> timeit("z = a.c", "from __main__ import a")
0.38655471766332994

Klasse __slots__mit a, b, c:

>>> timeit("z = b.c", "from __main__ import b")
0.14527461047146062

Wörterbuch mit den Tasten a, b, c:

>>> timeit("z = c['c']", "from __main__ import c")
0.11588272541098377

Tupel mit drei Werten unter Verwendung eines konstanten Schlüssels:

>>> timeit("z = d[2]", "from __main__ import d")
0.11106188992948773

Liste mit drei Werten unter Verwendung eines konstanten Schlüssels:

>>> timeit("z = e[2]", "from __main__ import e")
0.086038238242508669

Tupel mit drei Werten unter Verwendung eines lokalen Schlüssels:

>>> timeit("z = d[key]", "from __main__ import d, key")
0.11187358437882722

Liste mit drei Werten unter Verwendung eines lokalen Schlüssels:

>>> timeit("z = e[key]", "from __main__ import e, key")
0.088604143037173344

Gibt es irgendetwas an diesen kleinen timeitTests, das sie ungültig machen würde? Ich lief jedes Mal mehrmals, um sicherzustellen, dass kein zufälliges Systemereignis sie ausgelöst hatte und die Ergebnisse fast identisch waren.

Es scheint, dass Wörterbücher die beste Balance zwischen Leistung und Lesbarkeit bieten, wobei die Klassen an zweiter Stelle stehen. Dies ist bedauerlich, da ich für meine Zwecke auch das Objekt sequenzartig haben muss; daher meine Wahl von namedtuple.

Listen sind wesentlich schneller, aber konstante Schlüssel sind nicht wartbar. Ich müsste eine Reihe von Indexkonstanten erstellen, dh KEY_1 = 1, KEY_2 = 2 usw., was ebenfalls nicht ideal ist.

Bleibe ich bei diesen Entscheidungen oder gibt es eine Alternative, die ich verpasst habe?

DNS
quelle
1
Wenn Leistung eine solche Priorität hat, warum nicht C verwenden?
Skilldrick
10
@ Skilldrick: Dies ist nur ein kleiner Teil eines größeren Programms, das davon profitiert, in Python geschrieben zu sein. Das Umschreiben dieses Teils als C-Erweiterung ist eine Option, aber etwas unerwünscht, da auch anderer Code die Daten berührt, was die Dinge etwas kompliziert. Leistung ist wichtig, aber nicht so entscheidend; Ich wäre sehr zufrieden mit der 4-fachen Verbesserung, die Listen bieten, wenn nicht wegen der reduzierten Wartbarkeit. Ich suche nur nach anderen Optionen, bevor ich mich für einen Weg entscheide.
DNS
1
@ Warren P: Ja; Ich optimiere nicht vorzeitig. Dies ist eine sehr enge Schleife, in der der einfache Zugriff auf die Strukturen einen erheblichen Teil der Arbeit ausmacht. Es ist die langsamste verbleibende Schleife im Programm. Selbst eine bescheidene Verbesserung könnte die Laufzeit in der realen Welt um ein oder zwei Sekunden verkürzen. Da sich das Ganze wiederholt, summiert sich das.
DNS
1
Versuchen Sie auch, Pypy zu probieren. Bei Pypy habe ich keine Leistungsunterschiede zwischen den Fällen festgestellt.
Thomas Ahle
2
numpy hat eine Art von Strukturen und kann in einigen Fällen eine bessere Leistung als C liefern. docs.scipy.org/doc/numpy/user/basics.rec.html Ich habe dies und YMMV nicht ausprobiert!
Sam Watkins

Antworten:

55

Zu beachten ist, dass Namedtuples für den Zugriff als Tupel optimiert sind. Wenn Sie Ihren Accessor auf " a[2]statt" ändern a.c, wird eine ähnliche Leistung wie bei den Tupeln angezeigt. Der Grund dafür ist, dass die Namenszugriffe effektiv in Aufrufe an self [idx] übersetzt werden. Zahlen Sie also sowohl den Indexierungs- als auch den Namenssuchpreis .

Wenn Ihr Nutzungsmuster so ist , dass der Zugang von Namen gemeinsam ist, aber der Zugang als Tupel ist nicht, Sie könnten eine schnelles entsprechen namedtuple schreiben , die Dinge in die andere Richtung funktioniert: aufschiebt Index - Lookups für den Zugang von Namen. Dann zahlen Sie jedoch den Preis für die Indexsuche. ZB hier eine schnelle Implementierung:

def makestruct(name, fields):
    fields = fields.split()
    import textwrap
    template = textwrap.dedent("""\
    class {name}(object):
        __slots__ = {fields!r}
        def __init__(self, {args}):
            {self_fields} = {args}
        def __getitem__(self, idx): 
            return getattr(self, fields[idx])
    """).format(
        name=name,
        fields=fields,
        args=','.join(fields), 
        self_fields=','.join('self.' + f for f in fields))
    d = {'fields': fields}
    exec template in d
    return d[name]

Aber die Zeiten sind sehr schlecht, wenn __getitem__aufgerufen werden muss:

namedtuple.a  :  0.473686933517 
namedtuple[0] :  0.180409193039
struct.a      :  0.180846214294
struct[0]     :  1.32191514969

Das heißt, die gleiche Leistung wie eine __slots__Klasse für den Attributzugriff (nicht überraschend - das ist es), aber enorme Nachteile aufgrund der doppelten Suche bei indexbasierten Zugriffen. (Bemerkenswert ist, dass __slots__dies in Bezug auf die Geschwindigkeit nicht viel hilft. Es spart Speicher, aber die Zugriffszeit ist ohne sie ungefähr gleich.)

Eine dritte Möglichkeit wäre, die Daten zu duplizieren, z. Unterklasse aus Liste und speichern Sie die Werte sowohl in den Attributen als auch in den Listendaten. Sie erhalten jedoch keine listenäquivalente Leistung. Es ist ein großer Geschwindigkeitsschub, wenn man nur eine Unterklasse hat (Überprüfung auf reine Python-Überladungen). Daher dauert struct [0] in diesem Fall immer noch ungefähr 0,5 Sekunden (verglichen mit 0,18 für die Rohliste), und Sie verdoppeln die Speichernutzung, sodass sich dies möglicherweise nicht lohnt.

Brian
quelle
3
Vorsicht bei diesem Rezept, bei dem Felder Benutzereingabedaten enthalten können - die ausführenden Felder können beliebigen Code ausführen. Ansonsten super cool.
Michael Scott Cuthbert
13
Ist es nicht albern, dass der Zugriff nach Namen langsamer ist als der Zugriff nach Index für Namedtuples? Wenn ich ein NAMEDtuple implementiere, warum sollte ich den Zugriff per Index optimieren?
Rotareti
44

Diese Frage ist ziemlich alt (Internetzeit), daher dachte ich, ich würde versuchen, Ihren Test heute sowohl mit normalem CPython (2.7.6) als auch mit Pypy (2.2.1) zu duplizieren und zu sehen, wie die verschiedenen Methoden verglichen werden. (Ich habe auch eine indizierte Suche für das benannte Tupel hinzugefügt.)

Dies ist ein bisschen wie ein Mikro-Benchmark, also YMMV, aber Pypy schien den Zugriff auf benannte Tupel um den Faktor 30 gegenüber CPython zu beschleunigen (während der Zugriff auf Wörterbücher nur um den Faktor 3 beschleunigt wurde).

from collections import namedtuple

STest = namedtuple("TEST", "a b c")
a = STest(a=1,b=2,c=3)

class Test(object):
    __slots__ = ["a","b","c"]

    a=1
    b=2
    c=3

b = Test()

c = {'a':1, 'b':2, 'c':3}

d = (1,2,3)
e = [1,2,3]
f = (1,2,3)
g = [1,2,3]
key = 2

if __name__ == '__main__':
    from timeit import timeit

    print("Named tuple with a, b, c:")
    print(timeit("z = a.c", "from __main__ import a"))

    print("Named tuple, using index:")
    print(timeit("z = a[2]", "from __main__ import a"))

    print("Class using __slots__, with a, b, c:")
    print(timeit("z = b.c", "from __main__ import b"))

    print("Dictionary with keys a, b, c:")
    print(timeit("z = c['c']", "from __main__ import c"))

    print("Tuple with three values, using a constant key:")    
    print(timeit("z = d[2]", "from __main__ import d"))

    print("List with three values, using a constant key:")
    print(timeit("z = e[2]", "from __main__ import e"))

    print("Tuple with three values, using a local key:")
    print(timeit("z = d[key]", "from __main__ import d, key"))

    print("List with three values, using a local key:")
    print(timeit("z = e[key]", "from __main__ import e, key"))

Python-Ergebnisse:

Named tuple with a, b, c:
0.124072679784
Named tuple, using index:
0.0447055962367
Class using __slots__, with a, b, c:
0.0409136944224
Dictionary with keys a, b, c:
0.0412045334915
Tuple with three values, using a constant key:
0.0449477955531
List with three values, using a constant key:
0.0331083467148
Tuple with three values, using a local key:
0.0453569025139
List with three values, using a local key:
0.033030056702

PyPy-Ergebnisse:

Named tuple with a, b, c:
0.00444889068604
Named tuple, using index:
0.00265598297119
Class using __slots__, with a, b, c:
0.00208616256714
Dictionary with keys a, b, c:
0.013897895813
Tuple with three values, using a constant key:
0.00275301933289
List with three values, using a constant key:
0.002760887146
Tuple with three values, using a local key:
0.002769947052
List with three values, using a local key:
0.00278806686401
Gerrat
quelle
2
Interessanterweise ist bei Pypy das Wörterbuch das Schlimmste.
RomainL.
6

Dieses Problem ist möglicherweise bald veraltet. CPython dev hat offensichtlich die Leistung beim Zugriff auf benannte Tupelwerte nach Attributnamen erheblich verbessert. Die Änderungen sollen in Python 3.8 veröffentlicht werden Ende Oktober 2019 veröffentlicht werden.

Siehe: https://bugs.python.org/issue32492 und https://github.com/python/cpython/pull/10495 .

wst
quelle
1
Danke für die Information ! Zitat aus docs.python.org/3/whatsnew/3.8.html : "Beschleunigte Feldsuche in collection.namedtuple (). Sie sind jetzt mehr als zweimal schneller und damit die schnellste Form der Suche nach Instanzvariablen Python."
Ismael EL ATIFI
3

Ein paar Punkte und Ideen:

  1. Sie planen, mehrmals hintereinander auf denselben Index zuzugreifen. Ihr eigentliches Programm verwendet wahrscheinlich einen wahlfreien oder linearen Zugriff, der sich unterschiedlich verhält. Insbesondere wird es mehr CPU-Cache-Fehler geben. Mit Ihrem eigentlichen Programm erhalten Sie möglicherweise etwas andere Ergebnisse.

  2. OrderedDictionary wird als Wrapper geschrieben, daher ist dictes langsamer als dict. Das ist keine Lösung.

  3. Haben Sie sowohl Klassen im neuen als auch im alten Stil ausprobiert? (Klassen neuen Stils erben von object; Klassen alten Stils nicht)

  4. Haben Sie versucht, mit Psyco oder Unladen Swallow zu verwenden ? (Update 2020 - diese beiden Projekte sind tot)

  5. Ändert Ihre innere Schleife die Daten oder greift sie einfach darauf zu? Es ist möglicherweise möglich, die Daten vor dem Eintritt in die Schleife in die effizienteste Form umzuwandeln, aber verwenden Sie die bequemste Form an anderer Stelle im Programm.

Daniel Stutzbach
quelle
1

Ich wäre versucht, entweder (a) eine Art Workload-spezifisches Caching zu erfinden und das Speichern und Abrufen meiner Daten in einen memcachedb-ähnlichen Prozess zu verlagern, um die Skalierbarkeit zu verbessern, anstatt nur die Leistung zu erbringen, oder (b) als C-Erweiterung neu zu schreiben; mit nativer Datenspeicherung. Ein geordneter Wörterbuchtyp vielleicht.

Sie könnten damit beginnen: http://www.xs4all.nl/~anthon/Python/ordereddict/

Warren P.
quelle
-1

Sie können Ihre Klassen wie folgt sequenzieren, indem Sie __iter__und __getitem__Methoden hinzufügen , um sie wie folgt zu sortieren (indexierbar und iterierbar).

Würde eine OrderedDictArbeit? Es stehen mehrere Implementierungen zur Verfügung, die im Python31- collectionsModul enthalten sind.

mikerobi
quelle