In diesem Artikel stelle ich einige einfache Methoden vor, mit denen Python for-Schleifen 1,3- bis 900-mal schneller gemacht werden kann.
Eine häufig in Python integrierte Funktion ist das Timeit-Modul. Wir werden dies in den folgenden Abschnitten verwenden, um die aktuelle und verbesserte Leistung von Schleifen zu messen.
Für jede Methode haben wir eine Basislinie erstellt, indem wir einen Test durchgeführt haben, der darin bestand, die zu testende Funktion 100.000 Mal (Schleifen) über 10 Testläufe auszuführen und dann die durchschnittliche Zeit pro Schleife (in Nanosekunden, ns) zu berechnen.
Ein paar einfache Methoden
1. Listenverständnis
# Baseline version (Inefficient way)
# Calculating the power of numbers
# Without using List Comprehension
deftest_01_v0(numbers):
output= []
forninnumbers:
output.append(n**2.5)
returnoutput
# Improved version
# (Using List Comprehension)
deftest_01_v1(numbers):
output= [n**2.5forninnumbers]
returnoutput
Das Ergebnis ist wie folgt:
# Summary Of Test Results
Baseline: 32.158 ns per loop
Improved: 16.040 ns per loop
% Improvement: 50.1 %
Speedup: 2.00x
Sie sehen, dass die Verwendung von Listenverständnissen die Geschwindigkeit um das Zweifache erhöhen kann
2. Berechnen Sie die Länge extern
Wenn Sie sich bei der Iteration auf die Länge der Liste verlassen müssen, führen Sie die Berechnung außerhalb der for-Schleife durch.
# Baseline version (Inefficient way)
# (Length calculation inside for loop)
deftest_02_v0(numbers):
output_list= []
foriinrange(len(numbers)):
output_list.append(i*2)
returnoutput_list
# Improved version
# (Length calculation outside for loop)
deftest_02_v1(numbers):
my_list_length=len(numbers)
output_list= []
foriinrange(my_list_length):
output_list.append(i*2)
returnoutput_list
Durch das Verschieben der Listenlängenberechnung aus der for-Schleife wird sie um das 1,6-fache beschleunigt. Diese Methode kennen vielleicht nur wenige.
# Summary Of Test Results
Baseline: 112.135 ns per loop
Improved: 68.304 ns per loop
% Improvement: 39.1 %
Speedup: 1.64x
3. Verwenden Sie „Set“.
Verwenden Sie set im Falle eines Vergleichs mithilfe einer for-Schleife.
# Use for loops for nested lookups
deftest_03_v0(list_1, list_2):
# Baseline version (Inefficient way)
# (nested lookups using for loop)
common_items= []
foriteminlist_1:
ifiteminlist_2:
common_items.append(item)
returncommon_items
deftest_03_v1(list_1, list_2):
# Improved version
# (sets to replace nested lookups)
s_1=set(list_1)
s_2=set(list_2)
output_list= []
common_items=s_1.intersection(s_2)
returncommon_items
Die Verwendung von „set“ erhöht die Geschwindigkeit um das 498-fache, wenn für Vergleiche verschachtelte for-Schleifen verwendet werden
# Summary Of Test Results
Baseline: 9047.078 ns per loop
Improved: 18.161 ns per loop
% Improvement: 99.8 %
Speedup: 498.17x
4. Überspringen Sie irrelevante Iterationen
Vermeiden Sie redundante Berechnungen, d. h. überspringen Sie irrelevante Iterationen.
# Example of inefficient code used to find
# the first even square in a list of numbers
deffunction_do_something(numbers):
forninnumbers:
square=n*n
ifsquare%2==0:
returnsquare
returnNone # No even square found
# Example of improved code that
# finds result without redundant computations
deffunction_do_something_v1(numbers):
even_numbers= [iforninnumbersifn%2==0]
fornineven_numbers:
square=n*n
returnsquare
returnNone # No even square found
Diese Methode erfordert Codedesign beim Entwerfen des Inhalts der for-Schleife. Die spezifische Verbesserung kann je nach tatsächlicher Situation variieren:
# Summary Of Test Results
Baseline: 16.912 ns per loop
Improved: 8.697 ns per loop
% Improvement: 48.6 %
Speedup: 1.94x
5. Code-Zusammenführung
In einigen Fällen kann die direkte Einbindung des Codes einer einfachen Funktion in eine Schleife die Codekompaktheit und Ausführungsgeschwindigkeit verbessern.
# Example of inefficient code
# Loop that calls the is_prime function n times.
defis_prime(n):
ifn<=1:
returnFalse
foriinrange(2, int(n**0.5) +1):
ifn%i==0:
returnFalse
returnTrue
deftest_05_v0(n):
# Baseline version (Inefficient way)
# (calls the is_prime function n times)
count=0
foriinrange(2, n+1):
ifis_prime(i):
count+=1
returncount
deftest_05_v1(n):
# Improved version
# (inlines the logic of the is_prime function)
count=0
foriinrange(2, n+1):
ifi<=1:
continue
forjinrange(2, int(i**0.5) +1):
ifi%j==0:
break
else:
count+=1
returncount
Dieser kann sich auch um das 1,3-fache erhöhen
# Summary Of Test Results
Baseline: 1271.188 ns per loop
Improved: 939.603 ns per loop
% Improvement: 26.1 %
Speedup: 1.35x
Warum ist das?
Das Aufrufen von Funktionen ist mit Mehraufwand verbunden, z. B. dem Verschieben und Einfügen von Variablen auf den Stapel, Funktionssuchen und der Übergabe von Argumenten. Wenn eine einfache Funktion wiederholt in einer Schleife aufgerufen wird, erhöht sich der Overhead des Funktionsaufrufs und beeinträchtigt die Leistung. Durch die direkte Einbindung des Funktionscodes in die Schleife wird dieser Overhead eliminiert und die Geschwindigkeit möglicherweise erheblich verbessert.
⚠️Aber es sollte hier beachtet werden, dass es ein zu berücksichtigendes Problem ist, die Lesbarkeit des Codes und die Häufigkeit von Funktionsaufrufen in Einklang zu bringen.
einige Hinweise
6. Vermeiden Sie Doppelarbeit
Erwägen Sie, wiederholte Berechnungen zu vermeiden, da einige davon redundant sein und Ihren Code verlangsamen können. Erwägen Sie stattdessen ggf. eine Vorabberechnung.
deftest_07_v0(n):
# Example of inefficient code
# Repetitive calculation within nested loop
result=0
foriinrange(n):
forjinrange(n):
result+=i*j
returnresult
deftest_07_v1(n):
# Example of improved code
# Utilize precomputed values to help speedup
pv= [[i*jforjinrange(n)] foriinrange(n)]
result=0
foriinrange(n):
result+=sum(pv[i][:i+1])
returnresult
Die Ergebnisse sind wie folgt
# Summary Of Test Results
Baseline: 139.146 ns per loop
Improved: 92.325 ns per loop
% Improvement: 33.6 %
Speedup: 1.51x
7. Verwenden Sie Generatoren
Generatoren unterstützen die verzögerte Auswertung, was bedeutet, dass der darin enthaltene Ausdruck nur dann ausgewertet wird, wenn Sie den nächsten Wert von ihm anfordern. Die dynamische Verarbeitung von Daten kann dazu beitragen, die Speichernutzung zu reduzieren und die Leistung zu verbessern. Besonders bei großen Datenmengen
deftest_08_v0(n):
# Baseline version (Inefficient way)
# (Inefficiently calculates the nth Fibonacci
# number using a list)
ifn<=1:
returnn
f_list= [0, 1]
foriinrange(2, n+1):
f_list.append(f_list[i-1] +f_list[i-2])
returnf_list[n]
deftest_08_v1(n):
# Improved version
# (Efficiently calculates the nth Fibonacci
# number using a generator)
a, b=0, 1
for_inrange(n):
yielda
a, b=b, a+b
Sie können sehen, dass die Verbesserung offensichtlich ist:
# Summary Of Test Results
Baseline: 0.083 ns per loop
Improved: 0.004 ns per loop
% Improvement: 95.5 %
Speedup: 22.06x
8. map()-Funktion
Verwenden Sie die in Python integrierte Funktion „map()“. Es ermöglicht die Verarbeitung und Transformation aller Elemente in einem iterierbaren Objekt, ohne eine explizite for-Schleife zu verwenden.
defsome_function_X(x):
# This would normally be a function containing application logic
# which required it to be made into a separate function
# (for the purpose of this test, just calculate and return the square)
returnx**2
deftest_09_v0(numbers):
# Baseline version (Inefficient way)
output= []
foriinnumbers:
output.append(some_function_X(i))
returnoutput
deftest_09_v1(numbers):
# Improved version
# (Using Python's built-in map() function)
output=map(some_function_X, numbers)
returnoutput
Die Verwendung der in Python integrierten Funktion „map()“ anstelle einer expliziten for-Schleife erhöht die Geschwindigkeit um das 970-fache.
# Summary Of Test Results
Baseline: 4.402 ns per loop
Improved: 0.005 ns per loop
% Improvement: 99.9 %
Speedup: 970.69x
Warum ist das?
Die Funktion „map()“ ist in C geschrieben und stark optimiert, sodass ihre innere implizite Schleife viel effizienter ist als eine normale Python-for-Schleife. Die Geschwindigkeit hat sich also erhöht, oder man kann sagen, dass Python immer noch zu langsam ist, ha.
9. Verwenden Sie Memoization
Die Idee von Speicheroptimierungsalgorithmen besteht darin, die Ergebnisse teurer Funktionsaufrufe zwischenzuspeichern (oder zu „speichern“) und sie zurückzugeben, wenn dieselbe Eingabe erfolgt. Es kann redundante Berechnungen reduzieren und Programme beschleunigen.
Erstens ist die ineffiziente Version.
# Example of inefficient code
deffibonacci(n):
ifn==0:
return0
elifn==1:
return1
returnfibonacci(n-1) +fibonacci(n-2)
deftest_10_v0(list_of_numbers):
output= []
foriinnumbers:
output.append(fibonacci(i))
returnoutput
Dann verwenden wir die Funktion lru_cache der integrierten Functools von Python.
# Example of efficient code
# Using Python's functools' lru_cache function
importfunctools
@functools.lru_cache()
deffibonacci_v2(n):
ifn==0:
return0
elifn==1:
return1
returnfibonacci_v2(n-1) +fibonacci_v2(n-2)
def_test_10_v1(numbers):
output= []
foriinnumbers:
output.append(fibonacci_v2(i))
returnoutput
Das Ergebnis ist wie folgt:
# Summary Of Test Results
Baseline: 63.664 ns per loop
Improved: 1.104 ns per loop
% Improvement: 98.3 %
Speedup: 57.69x
Bei Verwendung der in Python integrierten Funktion functools nutzt die Funktion lru_cache die Memoisierung, um die Geschwindigkeit um das 57-fache zu erhöhen.
Wie wird die Funktion lru_cache implementiert?
„LRU“ ist die Abkürzung für „Least Recent Used“. lru_cache ist ein Dekorator, der auf Funktionen angewendet werden kann, um die Memoisierung zu ermöglichen. Es speichert die Ergebnisse der letzten Funktionsaufrufe in einem Cache, sodass die zwischengespeicherten Ergebnisse bereitgestellt werden können, wenn dieselbe Eingabe erneut angezeigt wird, wodurch Rechenzeit gespart wird. Die Funktion lru_cache ermöglicht bei Anwendung als Dekorator einen optionalen Parameter maxsize, der die maximale Größe des Caches bestimmt (d. h. für wie viele verschiedene Eingabewerte Ergebnisse gespeichert werden). Wenn der Parameter „maxsize“ auf „None“ gesetzt ist, ist die LRU-Funktion deaktiviert und der Cache kann uneingeschränkt wachsen, was viel Speicher verbraucht. Dies ist die einfachste Optimierungsmethode zum Austausch von Raum gegen Zeit.
10. Vektorisierung
importnumpyasnp
deftest_11_v0(n):
# Baseline version
# (Inefficient way of summing numbers in a range)
output=0
foriinrange(0, n):
output=output+i
returnoutput
deftest_11_v1(n):
# Improved version
# (# Efficient way of summing numbers in a range)
output=np.sum(np.arange(n))
returnoutput
Die Vektorisierung wird im Allgemeinen in den Datenverarbeitungsbibliotheken Numpy und Pandas des maschinellen Lernens verwendet.
# Summary Of Test Results
Baseline: 32.936 ns per loop
Improved: 1.171 ns per loop
% Improvement: 96.4 %
Speedup: 28.13x
11. Vermeiden Sie die Erstellung von Zwischenlisten
Verwenden Sie filterfalse, um die Erstellung von Zwischenlisten zu vermeiden. Es hilft, weniger Speicher zu verbrauchen.
deftest_12_v0(numbers):
# Baseline version (Inefficient way)
filtered_data= []
foriinnumbers:
filtered_data.extend(list(
filter(lambdax: x%5==0,
range(1, i**2))))
returnfiltered_data
Eine verbesserte Version derselben Funktionalität wird mithilfe der in Python integrierten itertools-Funktion filterfalse implementiert.
fromitertoolsimportfilterfalse
deftest_12_v1(numbers):
# Improved version
# (using filterfalse)
filtered_data= []
foriinnumbers:
filtered_data.extend(list(
filterfalse(lambdax: x%5!=0,
range(1, i**2))))
returnfiltered_data
Abhängig vom Anwendungsfall erhöht dieser Ansatz die Ausführungsgeschwindigkeit möglicherweise nicht wesentlich, kann jedoch die Speichernutzung verringern, indem die Erstellung von Zwischenlisten vermieden wird. Wir haben hier eine 131-fache Verbesserung erzielt
# Summary Of Test Results
Baseline: 333167.790 ns per loop
Improved: 2541.850 ns per loop
% Improvement: 99.2 %
Speedup: 131.07x
12. Effiziente Verbindungszeichenfolge
Jeder String-Verkettungsvorgang mit dem Operator „+“ ist langsam und verbraucht mehr Speicher. Verwenden Sie stattdessen „join“.
deftest_13_v0(l_strings):
# Baseline version (Inefficient way)
# (concatenation using the += operator)
output=""
fora_strinl_strings:
output+=a_str
returnoutput
deftest_13_v1(numbers):
# Improved version
# (using join)
output_list= []
fora_strinl_strings:
output_list.append(a_str)
return"".join(output_list)
Der Test benötigte eine einfache Möglichkeit, eine größere Liste von Zeichenfolgen zu generieren. Daher wurde eine einfache Hilfsfunktion geschrieben, um die Liste der für die Ausführung des Tests erforderlichen Zeichenfolgen zu generieren.
fromfakerimportFaker
defgenerate_fake_names(count : int=10000):
# Helper function used to generate a
# large-ish list of names
fake=Faker()
output_list= []
for_inrange(count):
output_list.append(fake.name())
returnoutput_list
l_strings=generate_fake_names(count=50000)
Das Ergebnis ist wie folgt:
# Summary Of Test Results
Baseline: 32.423 ns per loop
Improved: 21.051 ns per loop
% Improvement: 35.1 %
Speedup: 1.54x
Die Verwendung von Join-Funktionen anstelle des +-Operators beschleunigt die Verarbeitung um das 1,5-fache. Warum ist die Join-Funktion schneller?
Die zeitliche Komplexität der Zeichenfolgenverkettungsoperation unter Verwendung des +-Operators beträgt O(n²), während die zeitliche Komplexität der Zeichenfolgenverkettungsoperation unter Verwendung der Verknüpfungsfunktion O(n) beträgt.
Zusammenfassen
In diesem Artikel werden einige einfache Methoden vorgestellt, um die Leistung von Python-For-Schleifen um das 1,3- bis 970-fache zu verbessern.
- Die Verwendung der in Python integrierten Funktion „map()“ anstelle einer expliziten for-Schleife erhöht die Geschwindigkeit um das 970-fache
- Verwenden Sie set statt verschachtelter for-Schleifen, um die Geschwindigkeit um das 498-fache zu erhöhen [Tipp Nr. 3]
- Die Verwendung der Filterfalse-Funktion von itertools erhöht die Geschwindigkeit um das 131-fache
- Beschleunigen Sie die Geschwindigkeit um das 57-fache mit der Memoisierung mithilfe der Funktion lru_cache
https://avoid.overfit.cn/post/b01a152cfb824acc86f5118431201fe3
Autor: Nirmalya Ghosh