Copier une range(10**6)
liste aléatoire dix fois me prend environ 0,18 seconde: (ce sont cinq essais)
0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451
La copie de la liste non mélangée dix fois me prend environ 0,05 seconde:
0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184
Voici mon code de test:
from timeit import timeit
import random
a = range(10**6)
random.shuffle(a) # Remove this for the second test.
a = list(a) # Just an attempt to "normalize" the list.
for _ in range(5):
print timeit(lambda: list(a), number=10)
J'ai aussi essayé de copier avec a[:]
, les résultats étaient similaires (c'est-à-dire une grande différence de vitesse)
Pourquoi la grande différence de vitesse? Je connais et comprends la différence de vitesse dans le célèbre Pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié? exemple, mais ici mon traitement n'a pas de décisions. C'est juste copier aveuglément les références à l'intérieur de la liste, non?
J'utilise Python 2.7.12 sur Windows 10.
Edit: J'ai également essayé Python 3.5.2 maintenant, les résultats étaient presque les mêmes (mélangés de manière cohérente autour de 0,17 seconde, non mélangés de manière cohérente autour de 0,05 seconde). Voici le code pour cela:
a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
print(timeit(lambda: list(a), number=10))
la source
0.25
à chaque itération de chacun des tests. Donc, sur ma plate-forme, l'ordre compte.Réponses:
Le bit intéressant est que cela dépend de l'ordre dans lequel les entiers sont créés pour la première fois. Par exemple au lieu de
shuffle
créer une séquence aléatoire avecrandom.randint
:C'est aussi rapide que de copier votre
list(range(10**6))
(premier et rapide exemple).Cependant, lorsque vous mélangez - vos entiers ne sont plus dans l'ordre où ils ont été créés pour la première fois, c'est ce qui le ralentit.
Un rapide intermezzo:
Py_INCREF
inlist_slice
), donc Python a vraiment besoin d'aller là où se trouve l'objet. Il ne peut pas simplement copier la référence.Ainsi, lorsque vous copiez votre liste, vous obtenez chaque élément de cette liste et le mettez «tel quel» dans la nouvelle liste. Lorsque votre prochain élément a été créé peu de temps après celui en cours, il y a de fortes chances (aucune garantie!) Qu'il soit enregistré à côté de celui-ci sur le tas.
Supposons que chaque fois que votre ordinateur charge un élément dans le cache, il charge également les éléments
x
suivants en mémoire (localité du cache). Ensuite, votre ordinateur peut effectuer l'incrément de comptage de référence pour lesx+1
éléments sur le même cache!Avec la séquence mélangée, il charge toujours les éléments suivants en mémoire, mais ce ne sont pas les suivants dans la liste. Il ne peut donc pas effectuer l'incrément du nombre de références sans rechercher "vraiment" l'élément suivant.
TL; DR: La vitesse réelle dépend de ce qui s'est passé avant la copie: dans quel ordre ces éléments ont-ils été créés et dans quel ordre sont-ils dans la liste.
Vous pouvez le vérifier en regardant le
id
:Juste pour montrer un court extrait:
Donc, ces objets sont vraiment "côte à côte sur le tas". Avec
shuffle
ils ne sont pas:Ce qui montre que ceux-ci ne sont pas vraiment côte à côte dans la mémoire:
Note importante:
Je n'y ai pas pensé moi-même. La plupart des informations se trouvent dans le blog de Ricky Stewart .
Cette réponse est basée sur l'implémentation CPython "officielle" de Python. Les détails dans d'autres implémentations (Jython, PyPy, IronPython, ...) peuvent être différents. Merci @ JörgWMittag pour l'avoir signalé .
la source
list_slice
et à la ligne 453, vous pouvez voir l'Py_INCREF(v);
appel qui doit accéder à l'objet alloué au tas.a = [0] * 10**7
(au lieu de 10 ** 6 car c'était trop instable), qui est encore plus rapide que l'utilisationa = range(10**7)
(d'un facteur d'environ 1,25). Clairement parce que c'est encore mieux pour la mise en cache.[0,1,2,3]*((10**6) // 4)
est aussi rapide quea = [0] * 10**6
. Cependant, avec les entiers de 0 à 255, il y a un autre fait qui entre en jeu: ils sont internés donc avec eux l'ordre de création (à l'intérieur de votre script) n'est plus important - car ils sont créés lorsque vous démarrez python.Lorsque vous mélangez les éléments de la liste, leur localité de référence est moins bonne, ce qui entraîne de moins bonnes performances du cache.
Vous pourriez penser que la copie de la liste ne fait que copier les références, pas les objets, de sorte que leur emplacement sur le tas ne devrait pas avoir d'importance. Cependant, la copie implique toujours d'accéder à chaque objet afin de modifier le refcount.
la source
Comme expliqué par d' autres, ce n'est pas simplement copier les références , mais augmente également les comptes de référence à l' intérieur des objets et ainsi les objets sont accessibles et le cache joue un rôle.
Ici, je veux juste ajouter plus d'expériences. Pas tellement à propos de shuffled vs unshuffled (où accéder à un élément peut manquer le cache mais obtenir les éléments suivants dans le cache afin qu'ils soient touchés). Mais à propos des éléments répétitifs, où les accès ultérieurs du même élément peuvent atteindre le cache car l'élément est toujours dans le cache.
Test d'une plage normale:
Une liste de la même taille mais avec un seul élément répété encore et encore est plus rapide car elle atteint le cache tout le temps:
Et peu importe de quel numéro il s'agit:
Fait intéressant, cela devient encore plus rapide lorsque je répète à la place les mêmes deux ou quatre éléments:
Je suppose que quelque chose n'aime pas que le même compteur unique augmente tout le temps. Peut-être que certains pipelines se bloquent parce que chaque augmentation doit attendre le résultat de l'augmentation précédente, mais c'est une supposition sauvage.
Quoi qu'il en soit, essayez ceci pour un nombre encore plus grand d'éléments répétés:
La sortie (la première colonne est le nombre d'éléments différents, pour chaque je teste trois fois puis je prends la moyenne):
Donc, à partir d'environ 2,8 secondes pour un seul élément (répété), il tombe à environ 2,2 secondes pour 2, 4, 8, 16, ... éléments différents et reste à environ 2,2 secondes jusqu'à la centaine de milliers. Je pense que cela utilise mon cache L2 (4 × 256 Ko, j'ai un i7-6700 ).
Puis en quelques étapes, les temps passent à 3,5 secondes. Je pense que cela utilise un mélange de mon cache L2 et de mon cache L3 (8 Mo) jusqu'à ce qu'il soit également "épuisé".
À la fin, il reste à environ 3,5 secondes, je suppose que mes caches n'aident plus avec les éléments répétés.
la source
Avant le mélange, lorsqu'ils sont alloués dans le tas, les objets d'index adjacents sont adjacents en mémoire, et le taux de réussite de la mémoire est élevé lors de l'accès; après la lecture aléatoire, l'objet de l'index adjacent de la nouvelle liste n'est pas en mémoire. Adjacent, le taux de réussite est très faible.
la source