Array Performance très similaire à LinkedList – Qu’est-ce qui donne?

Donc, le titre est quelque peu trompeur … Je vais restr simple: je compare ces deux structures de données:

  1. Un tableau, où il commence à la taille 1, et pour chaque ajout suivant, il y a un appel realloc () pour étendre la mémoire, puis append le nouvel élément (malloced) à la position n-1.
  2. Une liste liée, par laquelle je garde la trace de la tête, de la queue et de la taille. Et l’ajout implique de faire un malloc pour un nouvel élément et de mettre à jour le pointeur et la taille de la queue.

Ne vous inquiétez pas des autres détails de ces structures de données. C’est la seule fonctionnalité qui me concerne pour ce test.

En théorie, la LL devrait être plus performante. Cependant, ils sont presque identiques dans les tests de temps impliquant 10, 100, 1000 … jusqu’à 5 000 000 d’éléments.

Mon sentiment est que le tas est grand. Je pense que le segment de données par défaut à 10 Mo sur Redhat? Je peux me tromper. Quoi qu’il en soit, realloc () vérifie d’abord si de l’espace est disponible à la fin de l’emplacement mémoire contigu déjà alloué (0- [n-1]). Si la nième position est disponible, les éléments ne sont pas déplacés. Au lieu de cela, realloc () réserve simplement l’ancien espace + l’espace immédiatement suivant. J’ai du mal à trouver des preuves de cela, et j’ai plus de mal à prouver que ce tableau devrait, dans la pratique, être moins performant que la LL.

Voici quelques parsings supplémentaires, après avoir lu les articles ci-dessous:

[Mise à jour # 1] J’ai modifié le code pour avoir une liste distincte de la mémoire mallocs à chaque 50e itération pour les LL et les tableaux. Pour 1 million d’ajouts au tableau, il y a presque toujours 18 mouvements. Il n’y a pas de concept de déplacement pour la LL. J’ai fait une comparaison de temps, ils sont encore presque identiques. Voici quelques sorties pour 10 millions d’ajouts:

(Array) time ./a.out a 10,000,000 real 0m31.266s user 0m4.482s sys 0m1.493s (LL) time ./a.out l 10,000,000 real 0m31.057s user 0m4.696s sys 0m1.297s 

Je m’attendrais à ce que les temps soient radicalement différents avec 18 mouvements. L’ajout de tableau nécessite une affectation supplémentaire et une comparaison de plus pour obtenir et vérifier la valeur de retour de realloc pour s’assurer qu’un déplacement a eu lieu.

[Update # 2] J’ai lancé un ltrace sur les tests que j’ai publiés ci-dessus, et je pense que c’est un résultat intéressant … Il semble que realloc (ou un gestionnaire de mémoire) déplace de manière préventive le tableau vers des emplacements contigus la taille actuelle. Pour 500 itérations, un mouvement de mémoire a été déclenché sur des itérations: 1, 2, 4, 7, 11, 18, 28, 43, 66, 101, 154, 235, 358, ce qui est assez proche d’une séquence de sommation. Je trouve cela très intéressant – je pensais le poster.

Donc, vous testez à quelle vitesse vous pouvez développer un tableau vers une liste chaînée?

Dans les deux cas, vous appelez une fonction d’allocation de mémoire. Généralement, les fonctions d’allocation de mémoire récupèrent une grande partie de la mémoire (peut-être une page) du système d’exploitation, puis la divisent en morceaux plus petits, comme requirejs par votre application.

L’autre hypothèse est que, de temps en temps, realloc () crache le mannequin et alloue une grande quantité de mémoire ailleurs car il ne peut pas y avoir de morceaux contigus dans la page actuellement allouée. Si vous ne faites pas passer d’autres appels à des fonctions d’allocation de mémoire entre votre liste, cela ne se produira pas. Et peut-être que l’utilisation de la mémoire virtuelle par votre système d’exploitation signifie que le segment de votre programme se développe de manière contiguë, indépendamment de l’origine des pages physiques. Dans ce cas, la performance sera identique à un tas d’appels malloc ().

Attendez-vous à ce que les performances changent là où vous mélangez les appels malloc () et realloc ().

Vous avez raison, realloc va simplement augmenter la taille du bloc alloué, sauf si cela est empêché. Dans un scénario réel, vous aurez probablement d’autres objects alloués sur le tas entre les ajouts suivants à la liste? Dans ce cas, realloc devra allouer un tout nouveau bloc de mémoire et copier les éléments déjà présents dans la liste.

Essayez d’allouer un autre object sur le tas en utilisant malloc pour chaque dizaine d’insertions et voyez si elles fonctionnent toujours de la même manière.

En supposant que votre liste liée soit un pointeur sur le premier élément, si vous souhaitez append un élément à la fin, vous devez d’abord parcourir la liste. Ceci est une opération O(n) .

En supposant que realloc doit déplacer le tableau vers un nouvel emplacement, il doit traverser le tableau pour le copier. Ceci est une opération O(n) .

En termes de complexité, les deux opérations sont égales. Cependant, comme d’autres l’ont fait remarquer, realloc évite peut-être de déplacer le tableau, auquel cas l’ajout de l’élément au tableau est O(1) . D’autres ont également souligné que la grande majorité du temps de votre programme est probablement passée dans malloc / realloc , que les deux implémentations appellent une fois par ajout.

Enfin, une autre raison pour laquelle la masortingce est probablement plus rapide est la cohérence du cache et la performance généralement élevée des copies linéaires. Sauter aux adresses erratiques avec des écarts importants entre eux (à la fois les éléments plus grands et la comptabilité malloc ) n’est généralement pas aussi rapide que de faire une copie en bloc du même volume de données.

Les performances d’une solution basée sur des tableaux développée avec realloc() dépendront de votre stratégie pour créer plus d’espace.

Si vous augmentez la quantité d’espace en ajoutant une quantité de stockage fixe à chaque réaffectation, vous obtiendrez une extension qui, en moyenne, dépend du nombre d’éléments stockés dans la baie. Ceci est supposé que realloc devra (occasionnellement) allouer de l’espace ailleurs et copier le contenu, plutôt que d’étendre simplement l’allocation existante.

Si vous augmentez la quantité d’espace en ajoutant une partie de votre nombre actuel d’éléments (le doublement est assez standard), vous vous retrouverez avec une expansion qui prend en moyenne du temps constant.

Le résultat du compilateur sera-t-il très différent dans ces deux cas?

Ce n’est pas une situation réelle. Vraisemblablement, dans la vraie vie, vous êtes intéressé à regarder ou même à supprimer des éléments de vos structures de données et à les append.

Si vous autorisez la suppression, mais uniquement à partir de la tête, la liste chaînée devient meilleure que le tableau car la suppression d’un élément est sortingviale et si au lieu de libérer l’élément supprimé, vous le placez sur une liste libre pour être recyclé, vous pouvez éliminer un Beaucoup de mallocs nécessaires lorsque vous ajoutez des éléments à la liste.

D’autre part, si vous avez besoin d’un access aléatoire à la structure, un tableau bat clairement la liste des liens.

(Mis à jour.) Comme d’autres l’ont noté, s’il n’y a pas d’autres allocations entre reallocs, aucune copie n’est nécessaire. Comme d’autres l’ont noté, le risque de copie de la mémoire diminue (mais aussi son impact bien sûr) sur les très petits blocs, plus petits qu’une page.

De plus, si tout ce que vous faites dans votre test est d’allouer un nouvel espace mémoire, je ne suis pas très surpris que vous voyiez peu de différence, car les appels système pour allouer de la mémoire prennent probablement la plupart du temps.

Choisissez plutôt vos structures de données en fonction de la manière dont vous souhaitez les utiliser. Un framebuffer est par exemple probablement mieux représenté par un tableau contigu.

Une liste chaînée est probablement meilleure si vous devez réorganiser ou sortinger rapidement les données dans la structure.

Ensuite, ces opérations seront plus ou moins efficaces selon ce que vous voulez faire.

(Merci pour les commentaires ci-dessous, je me suis d’abord trompé sur la façon dont ces choses fonctionnent.)

Quelle est la base de votre théorie selon laquelle la liste des liens devrait mieux fonctionner pour les insertions à la fin? Je ne m’attendrais pas à ce que, pour exactement la raison pour laquelle vous avez déclaré. realloc ne copiera que pour maintenir la contiguïté; dans d’autres cas, il peut être nécessaire de combiner des blocs libres et / ou d’augmenter la taille du bloc.

Cependant, chaque nœud de liste lié nécessite une nouvelle allocation et (en supposant une liste à double lien) deux écritures. Si vous voulez savoir comment fonctionne realloc , vous pouvez simplement comparer le pointeur avant et après realloc . Vous devriez trouver que cela ne change généralement pas.

Je soupçonne que puisque vous appelez realloc pour chaque élément (évidemment pas en production), l’appel realloc / malloc est le plus gros goulot d’étranglement pour les deux tests, même si realloc ne fournit souvent pas de nouveau pointeur.

En outre, vous confondez le segment de mémoire et le segment de données. Le tas est l’endroit où la mémoire malloced vit. Le segment de données concerne les variables globales et statiques.