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 timeit
Tests, 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?
Antworten:
Zu beachten ist, dass Namedtuples für den Zugriff als Tupel optimiert sind. Wenn Sie Ihren Accessor auf "
a[2]
statt" änderna.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.
quelle
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
quelle
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 .
quelle
Ein paar Punkte und Ideen:
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.
OrderedDictionary wird als Wrapper geschrieben, daher ist
dict
es langsamer alsdict
. Das ist keine Lösung.Haben Sie sowohl Klassen im neuen als auch im alten Stil ausprobiert? (Klassen neuen Stils erben von
object
; Klassen alten Stils nicht)Haben Sie versucht, mit Psyco oder Unladen Swallow zu verwenden ? (Update 2020 - diese beiden Projekte sind tot)
Ä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.
quelle
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/
quelle
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
OrderedDict
Arbeit? Es stehen mehrere Implementierungen zur Verfügung, die im Python31-collections
Modul enthalten sind.quelle