Maison  >  Article  >  développement back-end  >  timeit.repeat - jouer avec les répétitions pour comprendre les modèles

timeit.repeat - jouer avec les répétitions pour comprendre les modèles

王林
王林original
2024-08-09 07:25:021086parcourir

1. Le problème

Au cours de votre carrière en génie logiciel, vous pourriez rencontrer un morceau de code qui fonctionne mal et prend beaucoup plus de temps que ce qui est acceptable. Pour aggraver les choses, les performances sont incohérentes et assez variables selon plusieurs exécutions.

À l’heure actuelle, il faut accepter qu’en matière de performances logicielles, il y a beaucoup de non-déterminisme en jeu. Les données peuvent être distribuées dans une fenêtre et suivent parfois une distribution normale. D’autres fois, cela peut être irrégulier sans schéma évident.

2. L'approche

C'est à ce moment-là que l'analyse comparative entre en jeu. Exécuter votre code cinq fois, c'est bien, mais en fin de compte, vous ne disposez que de cinq points de données, avec trop de valeur accordée à chaque point de données. Nous avons besoin d'un nombre beaucoup plus grand de répétitions du même bloc de code pour voir un modèle.

3. La question

Combien de points de données faut-il avoir ? Beaucoup de choses ont été écrites à ce sujet, et l'un des articles que j'ai couverts

Une évaluation rigoureuse des performances nécessite la construction de repères,
exécuté et mesuré plusieurs fois afin de faire face au hasard
variation des délais d'exécution. Les chercheurs devraient fournir des mesures
de variation lors de la communication des résultats.

Kalibera, T. et Jones, R. (2013). Un benchmark rigoureux dans des délais raisonnables. Actes du Symposium international 2013 sur la gestion de la mémoire. https://doi.org/10.1145/2491894.2464160

Lors de la mesure des performances, nous souhaiterons peut-être mesurer l'utilisation du processeur, de la mémoire ou du disque pour obtenir une image plus large des performances. Il est généralement préférable de commencer par quelque chose de simple, comme le temps écoulé, car il est plus facile à visualiser. Une utilisation du processeur à 17 % ne nous dit pas grand-chose. Que devrait-il être ? 20% ou 5 ? L'utilisation du processeur n'est pas l'une des façons naturelles par lesquelles les humains perçoivent les performances.

4. L'expérience

Je vais utiliser la méthode timeit.repeat de python pour répéter un simple bloc d'exécution de code. Le bloc de code multiplie simplement les nombres de 1 à 2000.

from functools import reduce
reduce((lambda x, y: x * y), range(1, 2000))

Voici la signature de la méthode

(function) def repeat(
    stmt: _Stmt = "pass",
    setup: _Stmt = "pass",
    timer: _Timer = ...,
    repeat: int = 5,
    number: int = 1000000,
    globals: dict[str, Any] | None = None
) -> list[float]

Que sont la répétition et le numéro ?

Commençons par le nombre. Si le bloc de code est trop petit, il se terminera si rapidement que vous ne pourrez rien mesurer. Cet argument mentionne le nombre de fois que stmt doit être exécuté. Vous pouvez considérer cela comme le nouveau bloc de code. Le float renvoyé correspond au temps d'exécution du numéro stmt X.

Dans notre cas, nous garderons le nombre à 1000 puisque la multiplication jusqu'à 2000 coûte cher.

Ensuite, passez à la répétition. Ceci spécifie le nombre de répétitions ou le nombre de fois que le bloc ci-dessus doit être exécuté. Si la répétition vaut 5, alors la liste[float] renvoie 5 éléments.

Commençons par créer un bloc d'exécution simple

def run_experiment(number_of_repeats, number_of_runs=1000):
    execution_time = timeit.repeat(
        "from functools import reduce; reduce((lambda x, y: x * y), range(1, 2000))",
        repeat=number_of_repeats,
        number=number_of_runs
    )
    return execution_time

Nous voulons l'exécuter dans différentes valeurs de répétition

repeat_values = [5, 20, 100, 500, 3000, 10000]

Le code est assez simple et direct

5. Explorer les résultats

Nous arrivons maintenant à la partie la plus importante de l'expérience : l'interprétation des données. Veuillez noter que différentes personnes peuvent l'interpréter différemment et qu'il n'y a pas une seule bonne réponse.

Votre définition de la bonne réponse dépend beaucoup de ce que vous essayez d'accomplir. Êtes-vous préoccupé par la dégradation des performances de 95 % de vos utilisateurs ? Ou êtes-vous inquiet de la dégradation des performances de la queue de 5 % de vos utilisateurs qui sont assez vocaux ?

5.1. Statistiques d'analyse du temps d'exécution pour plusieurs valeurs de répétition

Comme on peut le constater, les temps min et max sont farfelus. Il montre comment un seul point de données peut suffire à modifier la valeur de la moyenne. Le pire, c'est que High Min et High Max correspondent à des valeurs de répétitions différentes. Il n’y a pas de corrélation et cela montre simplement le pouvoir des valeurs aberrantes.

Ensuite, nous passons à la médiane et remarquons qu'à mesure que nous augmentons le nombre de répétitions, la médiane diminue, sauf 20. Qu'est-ce qui peut l'expliquer ? Cela montre simplement à quel point un plus petit nombre de répétitions implique que nous n'obtenons pas nécessairement l'intégralité des valeurs possibles.

Passons à la moyenne tronquée, où les 2,5 % les plus bas et les 2,5 % les plus élevés sont tronqués. Ceci est utile lorsque vous ne vous souciez pas des utilisateurs aberrants et que vous souhaitez vous concentrer sur les performances des 95 % intermédiaires de vos utilisateurs.

Attention, essayer d'améliorer les performances des 95 % des utilisateurs moyens comporte la possibilité de dégrader les performances des 5 % des utilisateurs aberrants.

timeit.repeat - playing with repetitions to understand patterns

5.2. Execution Time Distribution for multiple values of repeat

Next we want to see where all the data lies. We would use histogram with bin of 10 to see where the data falls. With repetitions of 5 we see that they are mostly equally spaced. This is not one usually expects as sampled data should follow a normal looking distribution.

In our case the value is bounded on the lower side and unbounded on the upper side, since it will take more than 0 seconds to run any code, but there is no upper time limit. This means our distribution should look like a normal distribution with a long right tail.

Going forward with higher values of repeat, we see a tail emerging on the right. I would expect with higher number of repeat, there would be a single histogram bar, which is tall enough that outliers are overshadowed.

timeit.repeat - playing with repetitions to understand patterns

5.3. Execution Time Distribution for values 1000 and 3000

How about we look at larger values of repeat to get a sense? We see something unusual. With 1000 repeats, there are a lot of outliers past 1.8 and it looks a lot more tighter. The one on the right with 3000 repeat only goes upto 1.8 and has most of its data clustered around two peaks.

What can it mean? It can mean a lot of things including the fact that sometimes maybe the data gets cached and at times it does not. It can point to many other side effects of your code, which you might have never thought of. With the kind of distribution of both 1000 and 3000 repeats, I feel the TM95 for 3000 repeat is the most accurate value.

timeit.repeat - playing with repetitions to understand patterns


6. Appendix

6.1. Code

import timeit
import matplotlib.pyplot as plt
import json
import os
import statistics
import numpy as np

def run_experiment(number_of_repeats, number_of_runs=1000):
    execution_time = timeit.repeat(
        "from functools import reduce; reduce((lambda x, y: x * y), range(1, 2000))",
        repeat=number_of_repeats,
        number=number_of_runs
    )
    return execution_time

def save_result(result, repeats):
    filename = f'execution_time_results_{repeats}.json'
    with open(filename, 'w') as f:
        json.dump(result, f)

def load_result(repeats):
    filename = f'execution_time_results_{repeats}.json'
    if os.path.exists(filename):
        with open(filename, 'r') as f:
            return json.load(f)
    return None

def truncated_mean(data, percentile=95):
    data = np.array(data)
    lower_bound = np.percentile(data, (100 - percentile) / 2)
    upper_bound = np.percentile(data, 100 - (100 - percentile) / 2)
    return np.mean(data[(data >= lower_bound) & (data <= upper_bound)])

# List of number_of_repeats to test
repeat_values = [5, 20, 100, 500, 1000, 3000]

# Run experiments and collect results
results = []
for repeats in repeat_values:
    result = load_result(repeats)
    if result is None:
        print(f"Running experiment for {repeats} repeats...")
        try:
            result = run_experiment(repeats)
            save_result(result, repeats)
            print(f"Experiment for {repeats} repeats completed and saved.")
        except KeyboardInterrupt:
            print(f"\nExperiment for {repeats} repeats interrupted.")
            continue
    else:
        print(f"Loaded existing results for {repeats} repeats.")

    # Print time taken per repetition
    avg_time = statistics.mean(result)
    print(f"Average time per repetition for {repeats} repeats: {avg_time:.6f} seconds")

    results.append(result)

trunc_means = [truncated_mean(r) for r in results]
medians = [np.median(r) for r in results]
mins = [np.min(r) for r in results]
maxs = [np.max(r) for r in results]

# Create subplots
fig, axs = plt.subplots(2, 2, figsize=(15, 12))
fig.suptitle('Execution Time Analysis for Different Number of Repeats', fontsize=16)

metrics = [
    ('Truncated Mean (95%)', trunc_means),
    ('Median', medians),
    ('Min', mins),
    ('Max', maxs)
]

for (title, data), ax in zip(metrics, axs.flatten()):
    ax.plot(repeat_values, data, marker='o')
    ax.set_title(title)
    ax.set_xlabel('Number of Repeats')
    ax.set_ylabel('Execution Time (seconds)')
    ax.set_xscale('log')
    ax.grid(True, which="both", ls="-", alpha=0.2)

    # Set x-ticks and labels for each data point
    ax.set_xticks(repeat_values)
    ax.set_xticklabels(repeat_values)

    # Rotate x-axis labels for better readability
    ax.tick_params(axis='x', rotation=45)

plt.tight_layout()

# Save the plot to a file
plt.savefig('execution_time_analysis.png', dpi=300, bbox_inches='tight')
print("Plot saved as 'execution_time_analysis.png'")

# Create histograms for data distribution with 10 bins
fig, axs = plt.subplots(2, 3, figsize=(20, 12))
fig.suptitle('Data Distribution Histograms for Different Number of Repeats (10 bins)', fontsize=16)

for repeat, result, ax in zip(repeat_values, results, axs.flatten()):
    ax.hist(result, bins=10, edgecolor='black')
    ax.set_title(f'Repeats: {repeat}')
    ax.set_xlabel('Execution Time (seconds)')
    ax.set_ylabel('Frequency')

plt.tight_layout()

# Save the histograms to a file
plt.savefig('data_distribution_histograms_10bins.png', dpi=300, bbox_inches='tight')
print("Histograms saved as 'data_distribution_histograms_10bins.png'")

# Create histograms for 1000 and 3000 repeats with 30 bins
fig, axs = plt.subplots(1, 2, figsize=(15, 6))
fig.suptitle('Data Distribution Histograms for 1000 and 3000 Repeats (30 bins)', fontsize=16)

for repeat, result, ax in zip([1000, 3000], results[-2:], axs):
    ax.hist(result, bins=100, edgecolor='black')
    ax.set_title(f'Repeats: {repeat}')
    ax.set_xlabel('Execution Time (seconds)')
    ax.set_ylabel('Frequency')

plt.tight_layout()

# Save the detailed histograms to a file
plt.savefig('data_distribution_histograms_detailed.png', dpi=300, bbox_inches='tight')
print("Detailed histograms saved as 'data_distribution_histograms_detailed.png'")

plt.show()

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Tuple et ensembles en Python 4Article suivant:Tuple et ensembles en Python 4