12 Möglichkeiten, Python-Schleifen zu beschleunigen, die bis zu 900-mal schneller werden können

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

Je suppose que tu aimes

Origine blog.csdn.net/m0_46510245/article/details/135334893
conseillé
Classement