Benchmarking i profilowanie

W tym rozdziale dowiemy się, jak benchmarking i profilowanie pomagają w rozwiązywaniu problemów z wydajnością.

Załóżmy, że napisaliśmy kod, który również daje oczekiwany rezultat, ale co zrobić, jeśli chcemy uruchomić ten kod nieco szybciej, ponieważ potrzeby się zmieniły. W takim przypadku musimy dowiedzieć się, które części naszego kodu spowalniają cały program. W takim przypadku przydatne mogą być testy porównawcze i profilowanie.

Co to jest analiza porównawcza?

Benchmarking ma na celu ocenę czegoś przez porównanie ze standardem. Jednak pojawia się tutaj pytanie, czym byłby benchmarking i dlaczego potrzebujemy go w przypadku programowania oprogramowania. Benchmarking kodu oznacza, jak szybko kod jest wykonywany i gdzie znajduje się wąskie gardło. Jednym z głównych powodów testów porównawczych jest optymalizacja kodu.

Jak działa benchmarking?

Jeśli mówimy o działaniu benchmarkingu, musimy zacząć od analizy porównawczej całego programu jako jednego aktualnego stanu, a następnie możemy połączyć mikro testy porównawcze, a następnie rozłożyć program na mniejsze programy. Aby znaleźć wąskie gardła w naszym programie i zoptymalizować go. Innymi słowy, możemy to rozumieć jako rozbicie dużego i trudnego problemu na serię mniejszych i nieco łatwiejszych problemów w celu ich optymalizacji.

Moduł Pythona do testów porównawczych

W Pythonie mamy domyślnie moduł do testów porównawczych o nazwie timeit. Z pomocątimeit modułu, możemy zmierzyć wydajność małego fragmentu kodu Pythona w naszym głównym programie.

Przykład

W poniższym skrypcie w języku Python importujemy plik timeit moduł, który dodatkowo mierzy czas potrzebny do wykonania dwóch funkcji - functionA i functionB -

import timeit
import time
def functionA():
   print("Function A starts the execution:")
   print("Function A completes the execution:")
def functionB():
   print("Function B starts the execution")
   print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)

Po uruchomieniu powyższego skryptu otrzymamy czas wykonania obu funkcji, jak pokazano poniżej.

Wynik

Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076

Pisanie własnego timera za pomocą funkcji dekoratora

W Pythonie możemy stworzyć własny licznik czasu, który będzie działał tak samo jak timeitmoduł. Można to zrobić za pomocądecoratorfunkcjonować. Poniżej znajduje się przykład niestandardowego timera -

import random
import time

def timer_func(func):

   def function_timer(*args, **kwargs):
   start = time.time()
   value = func(*args, **kwargs)
   end = time.time()
   runtime = end - start
   msg = "{func} took {time} seconds to complete its execution."
      print(msg.format(func = func.__name__,time = runtime))
   return value
   return function_timer

@timer_func
def Myfunction():
   for x in range(5):
   sleep_time = random.choice(range(1,3))
   time.sleep(sleep_time)

if __name__ == '__main__':
   Myfunction()

Powyższy skrypt w Pythonie pomaga w importowaniu losowych modułów czasu. Stworzyliśmy funkcję dekoratora timer_func (). Wewnątrz znajduje się funkcja function_timer (). Teraz funkcja zagnieżdżona pobierze czas przed wywołaniem przekazanej funkcji. Następnie czeka na powrót funkcji i przechwytuje czas zakończenia. W ten sposób możemy wreszcie sprawić, że skrypt Pythona wypisze czas wykonania. Skrypt wygeneruje dane wyjściowe, jak pokazano poniżej.

Wynik

Myfunction took 8.000457763671875 seconds to complete its execution.

Co to jest profilowanie?

Czasami programista chce zmierzyć pewne atrybuty, takie jak użycie pamięci, złożoność czasowa lub użycie określonych instrukcji dotyczących programów, aby zmierzyć rzeczywiste możliwości tego programu. Takie mierzenie programu nazywa się profilowaniem. Profilowanie wykorzystuje dynamiczną analizę programu do takich pomiarów.

W kolejnych sekcjach dowiemy się o różnych modułach Pythona do profilowania.

cProfile - wbudowany moduł

cProfilejest wbudowanym modułem Pythona do profilowania. Moduł jest rozszerzeniem C z rozsądnym narzutem, co czyni go odpowiednim do profilowania długo działających programów. Po uruchomieniu rejestruje wszystkie funkcje i czasy wykonania. Jest bardzo potężny, ale czasami trochę trudny do zinterpretowania i wykonania. W poniższym przykładzie używamy cProfile w poniższym kodzie -

Przykład

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
   lock.acquire()
   increment_global()
   lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()

   t1 = threading.Thread(target=taskofThread, args=(lock,))
   t2 = threading.Thread(target= taskofThread, args=(lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
   print("x = {1} after Iteration {0}".format(i,x))

Powyższy kod jest zapisany w pliku thread_increment.pyplik. Teraz wykonaj kod z cProfile w wierszu poleceń w następujący sposób -

(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
      3577 function calls (3522 primitive calls) in 1.688 seconds

   Ordered by: standard name

   ncalls tottime percall cumtime percall filename:lineno(function)

   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
   … … … …

Z powyższego wyniku jasno wynika, że ​​cProfile wypisuje wszystkie 3577 wywołane funkcje, wraz z czasem spędzonym w każdej z nich oraz liczbą wywołań. Poniżej znajdują się kolumny, które otrzymaliśmy w wynikach -

  • ncalls - To liczba wykonanych połączeń.

  • tottime - Jest to całkowity czas spędzony w danej funkcji.

  • percall - Odnosi się do ilorazu czasu całkowitego podzielonego przez liczbę połączeń telefonicznych.

  • cumtime- Jest to łączny czas spędzony w tej i wszystkich podfunkcjach. Jest to nawet dokładne dla funkcji rekurencyjnych.

  • percall - Jest to iloraz czasu podzielonego przez prymitywne wywołania.

  • filename:lineno(function) - Zasadniczo zapewnia odpowiednie dane każdej funkcji.