web-dev-qa-db-fra.com

Python comparaison des performances pour la création d'ensembles - set () vs. {} literal

Une discussion après cette question m'a laissé perplexe, j'ai donc décidé d'exécuter quelques tests et de comparer le temps de création de set((x,y,z)) contre {x,y,z} Pour créer des ensembles dans = Python (J'utilise Python 3.7).

J'ai comparé les deux méthodes en utilisant time et timeit. Les deux étaient conformes * aux résultats suivants:

test1 = """
my_set1 = set((1, 2, 3))
"""
print(timeit(test1))

Résultat: 0,30240735499999993

test2 = """
my_set2 = {1,2,3}
"""
print(timeit(test2))

Résultat: 0.10771795900000003

La deuxième méthode était donc presque 3 fois plus rapide que la première. C'était une différence assez surprenante pour moi. Que se passe-t-il sous le capot pour optimiser les performances du littéral défini sur la méthode set() de cette manière? Quel serait souhaitable pour quels cas?

* Remarque: Je ne montre que les résultats des tests timeit car ils sont moyennés sur de nombreux échantillons, et donc peut-être plus fiables, mais les résultats lors des tests avec time ont montré des différences similaires dans les deux cas.


Edit: Je suis au courant de cette question similaire et bien qu'il réponde à certains aspects de ma question d'origine, il n'a pas couvrir tout cela. Les ensembles n'ont pas été abordés dans la question, et comme les ensembles vides n'ont pas de syntaxe littérale en python, j'étais curieux de savoir comment (le cas échéant) la création d'ensembles l'utilisation d'un littéral serait différente de l'utilisation de la méthode set(). De plus, je me suis demandé comment la manipulation du paramètre Tuple dans set((x,y,z) se passe en coulisses et quel est son impact possible sur l'exécution. La grande réponse de coldspeed a aidé à clarifier les choses.

18
yuvgin

(Ceci est en réponse au code qui a maintenant été édité hors de la question initiale) Vous avez oublié d'appeler les fonctions dans le deuxième cas. En apportant les modifications appropriées, les résultats sont comme prévu:

test1 = """
def foo1():
     my_set1 = set((1, 2, 3))
foo1()
"""    
timeit(test1)
# 0.48808742000255734
test2 = """
def foo2():
    my_set2 = {1,2,3}
foo2()
"""    
timeit(test2)
# 0.3064506609807722

Maintenant, la raison de la différence de synchronisation est due au fait que set() est un appel de fonction nécessitant une recherche dans la table des symboles, tandis que la construction d'ensemble {...} Est un artefact de la syntaxe et est beaucoup plus plus rapide.

La différence est évidente en observant le code d'octet démonté.

import dis

dis.dis("set((1, 2, 3))")
  1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               3 ((1, 2, 3))
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
dis.dis("{1, 2, 3}")
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_SET                3
              8 RETURN_VALUE

Dans le premier cas, un appel de fonction est effectué par l'instruction CALL_FUNCTION Sur le tuple (1, 2, 3) (Qui est également livré avec sa propre surcharge, bien que mineure - il est chargé en tant que constante via LOAD_CONST), Alors que dans la deuxième instruction, il n'y a qu'un appel BUILD_SET, Qui est plus efficace.

Re: votre question concernant le temps pris pour la construction de Tuple, nous voyons que cela est en fait négligeable:

timeit("""(1, 2, 3)""")
# 0.01858693000394851

timeit("""{1, 2, 3}""")
# 0.11971827200613916

Les tuples sont immuables, donc le compilateur optimise cette opération en la chargeant comme une constante - c'est ce qu'on appelle pliage constant (vous pouvez le voir clairement dans l'instruction LOAD_CONST Ci-dessus), donc l'heure prise est négligeable. Cela ne se voit pas avec les ensembles s'ils sont modifiables (merci à @ user2357112 de l'avoir signalé).


Pour des séquences plus grandes, nous voyons un comportement similaire. La syntaxe {..} Est plus rapide lors de la construction d'ensembles utilisant des compréhensions d'ensembles par opposition à set() qui doit construire l'ensemble à partir d'un générateur.

timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858

timeit("""{i for i in range(10000)}""", number=1000)
# 0.5508635920123197

Pour référence, vous pouvez également utiliser le déballage itérable sur les versions plus récentes:

timeit("""{*range(10000)}""", number=1000)
# 0.7462548640323803

Fait intéressant, cependant, set() est plus rapide lorsqu'il est appelé directement sur range:

timeit("""set(range(10000))""", number=1000)
# 0.3746800610097125

Cela s'avère plus rapide que la construction définie. Vous verrez un comportement similaire pour d'autres séquences (telles que lists).

Ma recommandation serait d'utiliser la compréhension d'ensemble {...} Lors de la construction de littéraux d'ensemble, et comme alternative au passage d'une compréhension de générateur à set(); et utilisez plutôt set() pour convertir une séquence existante/itérable en un ensemble.

31
cs95