J'utilisais un dictionnaire comme table de recherche, mais j'ai commencé à me demander si une liste serait meilleure pour mon application - la quantité d'entrées dans ma table de recherche n'était pas si grande. Je sais que les listes utilisent des tableaux C sous le capot, ce qui m'a fait conclure que la recherche dans une liste avec seulement quelques éléments serait meilleure que dans un dictionnaire (accéder à quelques éléments dans un tableau est plus rapide que calculer un hachage).
J'ai décidé de profiler les alternatives mais les résultats m'ont surpris. La recherche de liste n'était meilleure qu'avec un seul élément! Voir la figure suivante (tracé log-log):
Voici donc la question: Pourquoi les recherches de liste fonctionnent-elles si mal? Qu'est-ce qui me manque?
Sur une question secondaire, quelque chose d'autre qui a attiré mon attention était un peu de "discontinuité" dans le temps de recherche dict après environ 1000 entrées. J'ai tracé seul le temps de recherche du dict pour le montrer.
ps1 Je connais O(n) vs O(1) temps amorti pour les tableaux et les tables de hachage, mais c'est généralement le cas que pour un petit nombre d'éléments itérer sur un tableau, c'est mieux que d'utiliser une table de hachage.
p.s.2 Voici le code que j'ai utilisé pour comparer les temps de recherche dict et list:
import timeit
lengths = [2 ** i for i in xrange(15)]
list_time = []
dict_time = []
for l in lengths:
list_time.append(timeit.timeit('%i in d' % (l/2), 'd=range(%i)' % l))
dict_time.append(timeit.timeit('%i in d' % (l/2),
'd=dict.fromkeys(range(%i))' % l))
print l, list_time[-1], dict_time[-1]
p.s.3 Utilisation de Python 2.7.13
Je sais que les listes utilisent des tableaux C sous le capot, ce qui m'a fait conclure que la recherche dans une liste avec seulement quelques éléments serait meilleure que dans un dictionnaire (accéder à quelques éléments dans un tableau est plus rapide que calculer un hachage).
Accéder à quelques éléments du tableau est bon marché, bien sûr, mais le calcul ==
est étonnamment lourd en Python. Vous voyez ce pic dans votre deuxième graphique? Voilà le coût de l'informatique ==
pour deux pouces juste là.
Les recherches de votre liste doivent calculer ==
beaucoup plus que vos recherches de dict.
Pendant ce temps, le calcul des hachages peut être une opération assez lourde pour de nombreux objets, mais pour tous les ints impliqués ici, ils se hachent juste pour eux-mêmes. (-1 équivaudrait à -2, et les grands entiers (techniquement long
s) hacheraient à des entiers plus petits, mais cela ne s'applique pas ici.)
La recherche de dictée n'est pas vraiment si mauvaise en Python, surtout lorsque vos clés ne sont qu'une plage consécutive d'entiers. Toutes les entrées ici se hachent et Python utilise un schéma d'adressage ouvert personnalisé au lieu de chaîner, donc toutes vos clés se retrouvent presque aussi contiguës en mémoire que si vous aviez utilisé une liste (qui est c'est-à-dire que les pointeurs vers les clés se retrouvent dans une plage contiguë de PyDictEntry
s). La procédure de recherche est rapide et, dans vos cas de test, elle frappe toujours la bonne touche sur la première sonde.
D'accord, revenons au pic du graphique 2. Le pic des temps de recherche à 1024 entrées dans le deuxième graphique est dû au fait que pour toutes les tailles plus petites, les nombres entiers que vous cherchiez étaient tous <= 256, donc ils tombaient tous dans la plage de Petit cache entier de CPython. L'implémentation de référence de Python conserve les objets entiers canoniques pour tous les entiers de -5 à 256 inclus. Pour ces entiers, Python a pu utiliser un pointeur rapide comparaison pour éviter de passer par le processus (étonnamment lourd) de calcul ==
. Pour les entiers plus grands, l'argument de in
n'était plus le même objet que l'entier correspondant dans le dict, et Python devait parcourir l'intégralité de ==
processus.
La réponse courte est que les listes utilisent la recherche linéaire et les dits utilisent la recherche amortie O(1)).
De plus, les recherches dict peuvent ignorer un test d'égalité lorsque 1) les valeurs de hachage ne correspondent pas ou 2) lorsqu'il existe une correspondance d'identité. Les listes ne bénéficient que de l'optimisation de l'égalité sous-entendant l'identité.
En 2008, j'ai donné une conférence sur ce sujet où vous trouverez tous les détails: https://www.youtube.com/watch?v=hYUsssClE94
En gros, la logique de recherche dans les listes est la suivante:
for element in s:
if element is target:
# fast check for identity implies equality
return True
if element == target:
# slower check for actual equality
return True
return False
Pour les dicts, la logique est à peu près:
h = hash(target)
for i in probe_sequence(h, len(table)):
element = key_table[i]
if element is UNUSED:
raise KeyError(target)
if element is target:
# fast path for identity implies equality
return value_table[i]
if h != h_table[i]:
# unequal hashes implies unequal keys
continue
if element == target:
# slower check for actual equality
return value_table[i]
Les tables de hachage de dictionnaire sont généralement entre un tiers et deux tiers pleines, de sorte qu'elles ont tendance à avoir peu de collisions (quelques déplacements dans la boucle illustrée ci-dessus), quelle que soit leur taille. En outre, la vérification de la valeur de hachage empêche les vérifications d'égalité lentes inutiles (la probabilité d'une vérification d'égalité gaspillée est d'environ 1 sur 2 ** 64).
Si votre timing se concentre sur des entiers, il y a aussi d'autres effets en jeu. Ce hachage d'un int est l'int lui-même, donc le hachage est très rapide. En outre, cela signifie que si vous stockez des entiers consécutifs, il n'y a généralement aucune collision.
Vous dites "accéder à quelques éléments d'un tableau est plus rapide que de calculer un hachage".
Une règle de hachage simple pour les chaînes peut être juste une somme (avec un modulo à la fin). Il s'agit d'une opération sans branche qui peut se comparer favorablement aux comparaisons de caractères, surtout lorsqu'il y a une longue correspondance sur le préfixe.