web-dev-qa-db-fra.com

Pourquoi une "for" boucle est-elle tant plus rapide pour compter de vraies valeurs?

J'ai récemment répondu A Question sur un site sœur qui a demandé une fonction qui compte tous les chiffres même d'un nombre. L'un des Autres réponses contenait deux fonctions (ce qui s'est avéré être le plus rapide, jusqu'à présent):

def count_even_digits_spyr03_for(n):
    count = 0
    for c in str(n):
        if c in "02468":
            count += 1
    return count

def count_even_digits_spyr03_sum(n):
    return sum(c in "02468" for c in str(n))

De plus, j'ai examiné à l'aide d'une compréhension de liste et de list.count:

def count_even_digits_spyr03_list(n):
    return [c in "02468" for c in str(n)].count(True)

Les deux premières fonctions sont essentiellement identiques, sauf que la première utilise une boucle de comptage explicite, tandis que la seconde utilise l'intégré sum. J'aurais attendu que le second soit plus rapide (basé sur E.G. cette réponse ), et c'est ce que j'aurais recommandé de transformer le premier en si demandé si vous avez demandé un examen. Mais, il s'avère que c'est l'inverse. Testez-le avec des nombres aléatoires avec un nombre croissant de chiffres (donc la chance que tout chiffre soit égal à environ 50%), je reçois les horaires suivants:

enter image description here

Pourquoi le manuel for boucle est tellement plus rapide? C'est presque un facteur deux plus rapide que d'utiliser sum. Et puisque l'intégré sum doit être environ cinq fois plus rapide que de sommation manuelle une liste (selon la réponse liée ), cela signifie qu'il est en fait dix fois plus rapide! Est-ce que la sauvegarde ne doit-elle ajouter qu'un au comptoir pour la moitié des valeurs, car l'autre moitié est jetée, suffisamment pour expliquer cette différence?


Utilisation d'un if comme filtre comme:

def count_even_digits_spyr03_sum2(n):
    return sum(1 for c in str(n) if c in "02468")

Améliore le timing uniquement au même niveau que la compréhension de la liste.


Lors de l'étendue des horaires à des nombres plus importants et de normaliser le calendrier de boucle for, ils convergent asymptotiquement pour de très grands nombres (> 10k chiffres), probablement dus au temps str(n) prend:

enter image description here

56
Graipher

Toutes vos fonctions contiennent un nombre égal d'appels à str(n) (un appel) et c in "02468" (Pour chaque C IN n). Depuis lors, je voudrais simplifier:

import timeit

num = ''.join(str(i % 10) for i in range(1, 10000001))

def count_simple_sum():
    return sum(1 for c in num)

def count_simple_for():
    count = 0
    for c in num:
        count += 1
    return count


print('For Loop Sum:', timeit.timeit(count_simple_for, number=10))
print('Built-in Sum:', timeit.timeit(count_simple_sum, number=10))

sum est toujours plus lent:

For Loop Sum: 2.8987821330083534
Built-in Sum: 3.245505138998851

La différence clé entre ces deux fonctions est que, dans count_simple_for, Vous ne jetez que jetez num _ avec pur pour la boucle for c in num, Mais dans count_simple_sum Vous créez une generator objet ici (à partir de @ Markus Meskanen Réponse avec dis.dis ):

  3 LOAD_CONST               1 (<code object <genexpr> at 0x10dcc8c90, file "test.py", line 14>)
  6 LOAD_CONST               2 ('count2.<locals>.<genexpr>')

sum _ est itération sur cet objet générateur à résumer les éléments produits et ce générateur est itération des éléments sur Num pour produire 1 sur chaque élément. Avoir une autre étape d'itération est chère car elle nécessite d'appeler generator.__next__() sur chaque élément et ces appels sont placés dans le bloc de try: ... except StopIteration: Qui ajoute également des frais généraux.

0
sanyassh