pip install pytest
pip install pytest-benchmark
W celu pomiaru czasu można wykorzystać bibliotekę pytest
. Przykład użycia na podstawie trzech sposobów łączenia ze sobą list:
def naive_concatenate(destination, source, destination_offset, source_offset):
while source_offset < len(source):
destination[destination_offset] = source[source_offset]
destination_offset += 1
source_offset += 1
return destination
def in_place_concatenate(destination, source, destination_offset, source_offset):
to_concat = source[source_offset:]
destination[destination_offset : destination_offset + len(to_concat)] = to_concat
return destination
def new_list_concatenate(destination, source, destination_offset, source_offset):
to_concat = source[source_offset:]
return (
destination[:destination_offset] + to_concat + destination[destination_offset + len(to_concat) :]
)
Pytest
automatycznie uruchamia kod wiele razy, aby zminimalizować szum środowiskowy (np. ze stanu procesora, procesów w tle itp.), który może zniekształcać pomiary czasu wykonania. Wykonuje rozgrzewkę (cache warm-up) i odrzuca kilka pierwszych powolnych przebiegów (spowodowanych inicjalizacją lub brakiem pamięci podręcznej).
from concatenation import (
version,
naive_concatenate,
new_list_concatenate,
in_place_concatenate,
)
import pytest
def test_version():
assert version == "0.1.0"
@pytest.fixture
def concat_data():
destination_list = list(range(10000))
source_list = list(range(1000, 8000))
result = list(range(1500, 8000)) + list(range(6500, 10000))
return destination_list, source_list, result
def test_naive_concatenate(concat_data, benchmark):
assert benchmark(naive_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]
def test_new_list_concatenate(concat_data, benchmark):
assert concat_data[0] != concat_data[-1]
assert benchmark(new_list_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]
def test_in_place_concatenate(concat_data, benchmark):
assert concat_data[0] != concat_data[-1]
assert benchmark(in_place_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]
Ręczny pomiar czasu wykonania zazwyczaj daje pojedynczą liczbę, ale pytest-benchmark
dostarcza szczegółowych statystyk:
Aby biblioteka zapisała wyniki do pliku należy podczas uruchamiania skryptu dodać opcję:
--benchmark-save=benchmark_results
Wówczas nastąpi stworzenie pliku JSON o nazwie benchmark_results
w katalogu .benchmarks
z pełnymi wynikami wszystkich testów. Przykładowy fragment pliku wynikowego JSON:
"benchmarks": [
{
"group": null,
"name": "test_naive_concatenate",
"fullname": "test_concatenation.py::test_naive_concatenate",
"params": null,
"param": null,
"extra_info": {},
"options": {
"disable_gc": false,
"timer": "perf_counter",
"min_rounds": 5,
"max_time": 1.0,
"min_time": 5e-06,
"warmup": false
},
"stats": {
"min": 0.00022297000396065414,
"max": 0.0008049440002650954,
"mean": 0.00025703103677145417,
"stddev": 3.915618459680731e-05,
"rounds": 3115,
"median": 0.0002536410029279068,
"iqr": 1.9296248865430243e-05,
"q1": 0.0002421549997961847,
"q3": 0.00026145124866161495,
"iqr_outliers": 149,
"stddev_outliers": 118,
"outliers": "118;149",
"ld15iqr": 0.00022297000396065414,
"hd15iqr": 0.0002904470020439476,
"ops": 3890.5807351552485,
"total": 0.8006516795430798,
"iterations": 1
}
},
Posiadając wynikowy plik ze statystykami pomiaru czasów dla wszystkich testów można dokonać wykreślenia wykresów z pomocą biblioteki matplotlib
W niektórych przypadkach może okazać się, że użycie standarodwej funkcji benchmark
jest niewystarczające, należy wówczas użyć funkcji benchmark.pedantic
do bardziej precyzyjnego i kontrolowanego mierzenia wydajności kodu.
Argument setup
w tej funkcji pozwala na określenie kodu przygotowawczego, który zostanie wykonany przed każdym wywołaniem testowanego fragmentu kodu, ale nie będzie wliczany do czasu mierzonego benchmarku. Jest to szczególnie przydatne w sytuacjach:
Przykład:
Załóżmy że posiadamy klasę MyList
która implementuje prostą listę z metodami do dołączania elementów na końcu listy (append
), umieszczania elementów na zadanej pozycji (insert
) oraz sprawdzania jej długości (__len__
):
class MyList:
def __init__(self):
self.backend = []
def append(self, item):
self.backend.append(item)
def insert(self, position, item):
self.backend.insert(position, item)
def __len__(self):
return len(self.backend)
Jeśli chcemy przeprowadzić pomiary czasowe dla metod klasy MyList
, narzucającą się, choć błędną ich implementacją będzie:
import pytest
from my_list import MyList
def test_append():
test_list = MyList()
assert len(test_list) == 0
test_list.append(7)
test_list.append(8)
assert len(test_list) == 2
@pytest.mark.benchmark(group="append")
def test_benchmark_append(benchmark):
test_list = MyList()
benchmark(test_list.append, 7)
@pytest.mark.benchmark(group="insert")
def test_benchmark_insert(benchmark):
test_list = MyList()
benchmark(lambda x: test_list.insert(0, x), 7)
Obiekt test_list
będący instancją klasy MyList
przekazywany jest do funkcji benchmark
. Biblioteka Pytest
starając się wyznaczyć statystki czasowe, wywołuje metody klasy MyList
wielokrotnie. Aby możliwe było wyznaczenie tych statystyk, zmierzony czas w kolejnych uruchomieniach musi oscylować wokół tych samych wartości, bądź zbiegać do pewnej konkretnej wartości czasowej. W przypadku funkcji testowych test_benchmark_append
oraz test_benchmark_insert
czas ten z każdym kolejnym uruchomieniem będzie rósł, gdyż kolejne iteracje testu będą przeprowadzane na tej samej instancji klasy MyList
, a więc liczba elementów startowych listy w każdej kolejnej iteracji będzie o 1 większa. Rozwiązaniem tego problemu jest użycie funkcji testowej benchmark.pedantic
z argumentem setup
:
@pytest.mark.benchmark(group="append")
def test_benchmark_append_improved(benchmark):
def setup():
return (MyList(), 7), {}
benchmark.pedantic(MyList.append, setup=setup)
@pytest.mark.benchmark(group="insert")
def test_benchmark_insert_improved(benchmark):
def setup():
return (MyList(), 0, 7), {}
benchmark.pedantic(MyList.insert, setup=setup)