web-dev-qa-db-fra.com

Quel est le taux de croissance idéal pour un tableau alloué de manière dynamique?

C++ a STD :: Vector and Java a une arraylist et de nombreuses autres langues ont leur propre forme de matrice allouée dynamiquement. Lorsqu'un tableau dynamique est sorti de l'espace, il est réaffecté dans une zone plus grande et Les anciennes valeurs sont copiées dans la nouvelle matrice. Une question centrale à la performance d'un tel tableau est de la rapidité avec laquelle la matrice se développe de taille. Si vous ne comprenez toujours que suffisamment grand pour vous adapter à la poussée actuelle, vous finirez par réaffecter à chaque fois . Donc, il est logique de doubler la taille de la matrice ou de la multiplier par dire 1.5x.

Existe-t-il un facteur de croissance idéal? 2x? 1.5x? Par idéal, je veux dire mathématiquement justifié, meilleure performance d'équilibrage et mémoire gaspillée. Je me rends compte que théoriquement, étant donné que votre demande pourrait avoir une distribution potentielle de pousses que celle-ci est quelque peu dépendante. Mais je suis curieux de savoir s'il y a une valeur "généralement" le mieux ou est considérée comme la meilleure dans une certaine contrainte rigoureuse.

J'ai entendu dire qu'il y a un papier sur cela quelque part, mais j'ai été incapable de le trouver.

73
Joseph Garvin

Je me souviens de lire il y a de nombreuses années Pourquoi 1.5 est préféré sur deux, au moins comme appliqué à C++ (cela ne s'applique probablement pas aux langages gérés, où le système d'exécution peut déplacer des objets à volonté).

Le raisonnement est-ce:

  1. Disons que vous commencez avec une allocation de 16 octets.
  2. Lorsque vous avez besoin de plus, vous allouez 32 octets, puis libérez 16 octets. Cela laisse un trou de 16 octets en mémoire.
  3. Lorsque vous avez besoin de plus, vous allouez 64 octets, libérant ainsi les 32 octets. Cela laisse un trou de 48 octets (si les 16 et 32 ​​étaient adjacents).
  4. Lorsque vous avez besoin de plus, vous allez allouer 128 octets, libérant ainsi les 64 octets. Cela laisse un trou de 112 octets (en supposant que toutes les allocations précédentes sont adjacentes).
  5. Et ainsi et ainsi de suite.

L'idée est que, avec une expansion 2x, il n'ya aucun point à temps que le trou résultant ne sera jamais assez grand pour réutiliser pour la prochaine allocation. En utilisant une allocation de 1.5x, nous avons cela à la place:

  1. Commencez avec 16 octets.
  2. Lorsque vous avez besoin de plus, allouez 24 octets, puis libérez le 16, laissant un trou de 16 octets.
  3. Lorsque vous avez besoin de plus, allouer 36 octets, puis libérer le 24, laissant un trou de 40 octets.
  4. Lorsque vous avez besoin de plus, allouez 54 octets, puis libérez les 36, laissant un trou de 76 octets.
  5. Lorsque vous avez besoin de plus, allouer 81 octets, puis libérer les 54, laissant un trou de 130 octets.
  6. Lorsque vous avez besoin de plus, utilisez 122 octets (arrondir) à partir du trou de 130 octets.
88
Chris Jester-Young

Une approche lors de la réponse des questions comme celle-ci est de "tricher" et de regarder quelles bibliothèques populaires font, sous l'hypothèse selon laquelle une bibliothèque largement utilisée est, à tout le moins, ne faisant pas quelque chose d'horrible.

Alors, vérifie juste très vite, Ruby (1.9.1-P129) semble utiliser 1.5x lorsque vous admettez une matrice, et Python (2.6.2) utilise 1.125x plus une constante (en Objects/listobject.c ):

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
    PyErr_NoMemory();
    return -1;
} else {
    new_allocated += newsize;
}

newsize ci-dessus est le nombre d'éléments dans la matrice. Notez bien que newsize est ajouté à new_allocated, donc l'expression avec le bitShifts et l'opérateur ternaire ne constitue vraiment que de calculer la surallocation.

11
Jason Creighton

Disons que vous cultivez la taille de la matrice par x. Assumez donc que vous commencez par la taille T. La prochaine fois que vous cultivez la matrice, sa taille sera T*x. Ensuite, ce sera T*x^2 Et ainsi de suite.

Si votre objectif est de pouvoir réutiliser la mémoire créée précédemment, vous souhaitez vous assurer que la nouvelle mémoire que vous allouez est inférieure à la somme de la mémoire précédente que vous avez déroutée. Par conséquent, nous avons cette inégalité:

T*x^n <= T + T*x + T*x^2 + ... + T*x^(n-2)

Nous pouvons enlever T des deux côtés. Nous obtenons donc ceci:

x^n <= 1 + x + x^2 + ... + x^(n-2)

De manière informelle, ce que nous disons, c'est qu'à nth _ allocation, nous voulons que notre toute la mémoire précédemment traitée soit supérieure ou égale à la mémoire de la mémoire à la nième allocation afin que nous puissions réutiliser la mémoire précédemment traitée.

Par exemple, si nous voulons pouvoir faire cela à la 3ème étape (c'est-à-dire n=3), Alors nous avons

x^3 <= 1 + x 

Cette équation est vraie pour tous x tels que 0 < x <= 1.3 (À peu près)

Voir ce que X nous obtenons pour différents N ci-dessous:

n  maximum-x (roughly)

3  1.3

4  1.4

5  1.53

6  1.57

7  1.59

22 1.61

Notez que le facteur de croissance doit être inférieur à 2 Depuis x^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2.

7
CEGRD

Cela dépend vraiment. Certaines personnes analysent des cas d'utilisation courants pour trouver le nombre optimal.

J'ai vu 1,5x 2.0x Phi X et la puissance de 2 utilisée auparavant.

4
Unknown

Si vous avez une distribution sur des longueurs de tableau, vous avez une fonction d'utilité qui dit à quel point vous aimez gaspiller de l'espace contre la perte de temps, vous pouvez également choisir une stratégie optimale de redimensionnement (et de dimensionnement initial).

La raison pour laquelle le multiple constant simple est utilisé, est évidemment pour que chaque annexe ait amorti du temps constant. Mais cela ne signifie pas que vous ne pouvez pas utiliser un rapport différent (plus grand) pour les petites tailles.

Dans SCALA, vous pouvez remplacer LoadFactor pour les tables de hachage de bibliothèque standard avec une fonction qui ressemble à la taille actuelle. Curieusement, les matrices redimensionnables sont juste doubles, ce qui est ce que la plupart des gens font dans la pratique.

Je ne connais pas de tableaux de doublement (ou de 1.5 * ing) qui rattrapent des erreurs de mémoire et se développent moins dans ce cas. Il semble que si vous aviez un énorme tableau unique, vous voudrez le faire.

J'ajouterais davantage que si vous gardez les matrices redimensables autour assez longtemps et que vous privilégiez l'espace au fil du temps, il peut être logique de s'allonger considérablement (pour la plupart des cas) d'abord, puis de réaffecter exactement la bonne taille lorsque vous êtes terminé.

2
Jonathan Graehl

Je suis d'accord avec Jon Skeet, même mon ami de théorycrafter insiste sur le fait que cela peut être prouvé être O(1) lors de la définition du facteur 2x.

Le rapport entre le temps de processeur et la mémoire est différent sur chaque machine, et le facteur varie donc autant. Si vous avez une machine avec des gigaoctets de RAM et une CPU lente, la copie des éléments à un nouveau tableau est beaucoup plus chère que sur une machine rapide, ce qui pourrait à son tour avoir moins de mémoire. C'est une question qui peut être répondue en théorie, pour un ordinateur uniforme, qui dans de vrais scénarios ne vous aident pas du tout.

1
Tom

Je sais que c'est une vieille question, mais il y a plusieurs choses que tout le monde semble manquer.

Tout d'abord, c'est la multiplication par 2: taille << 1. Ceci est multiplication par n'importe quoi entre 1 et 2: Int (flotteur (taille) * x), où x est le nombre, le * flotte Point Math, et le processeur doit exécuter des instructions supplémentaires pour la coulée entre flotteur et INT. En d'autres termes, au niveau de la machine, le doublage prend une seule instruction très rapide pour trouver la nouvelle taille. En multipliant par quelque chose entre 1 et 2 nécessite au moins une instruction à la taille de la taille d'un flotteur, une instruction à multiplier (qui est une multiplication de flotteur, il faut donc au moins deux fois plus de cycles, sinon 4 voire 8 fois plus), et une instruction de renvoyer à l'int, et suppose que votre plate-forme peut effectuer des mathématiques flottantes sur les registres à usage général, au lieu de nécessiter l'utilisation de registres spéciaux. En bref, vous devez vous attendre à ce que les mathématiques pour chaque allocation prennent au moins 10 fois plus longtemps qu'un simple changement de gauche. Si vous copiez beaucoup de données lors de la réaffectation cependant, cela pourrait ne pas faire une grande partie de la différence.

Deuxièmement, et probablement le grand kicker: tout le monde semble supposer que la mémoire libérée est à la fois contiguë avec elle-même, ainsi que contiguë avec la mémoire nouvellement attribuée. Sauf si vous préalez vous-même toute la mémoire vous-même, puis utilisez-la comme piscine, ce n'est presque certainement pas le cas. Le système d'exploitation pourrait occasionnellement finalement faire cela, mais la plupart du temps, il y aura suffisamment de fragmentation de l'espace libre que tout système de gestion de la mémoire à moitié décent sera en mesure de trouver un petit trou où votre mémoire sera Juste en forme. Une fois que vous avez eu des morceaux vraiment morts, vous êtes plus susceptible de vous retrouver avec des morceaux contigus, mais à ce moment-là, vos allocations sont assez grandes que vous ne les faites plus souvent suffisamment pour que cela soit plus important. En bref, il est amusant d'imaginer que l'utilisation d'un nombre idéal permettra l'utilisation la plus efficace de l'espace mémoire libre, mais en réalité, cela ne se produira que si votre programme est en cours d'exécution sur le métal nu (comme dans, il n'y a pas de système d'exploitation sous elle apportant toutes les décisions).

Ma réponse à la question? Nope, il n'y a pas de nombre idéal. C'est tellement une application spécifique que personne ne tente vraiment même. Si votre objectif est une utilisation idéale de la mémoire, vous avez à peu près de la chance. Pour la performance, les allocations moins fréquentes sont meilleures, mais si nous allions juste avec cela, nous pourrions nous multiplier par 4 ou même 8! Bien sûr, lorsque Firefox saute de 1 Go à 8 Go en un coup, les gens vont se plaindre, de sorte que cela n'a même pas de sens. Voici quelques règles de pouce que j'irais bien que:

Si vous ne pouvez pas optimiser l'utilisation de la mémoire, du moins ne gaspillez pas les cycles de processeur. Multiplier par 2 est au moins un ordre de grandeur plus rapide que de faire des mathématiques de point flottant. Cela pourrait ne pas faire une énorme différence, mais cela fera une différence au moins (particulièrement tôt, pendant les allocations les plus fréquentes et les plus petites).

Ne pas trop penser. Si vous venez de passer 4 heures à essayer de déterminer comment faire quelque chose qui a déjà été fait, vous venez de perdre votre temps. Totalement honnêtement, s'il y avait une meilleure option que * 2, il aurait été fait dans la classe de vecteur C++ (et de nombreux autres endroits) il y a plusieurs décennies.

Enfin, si vous vraiment Voulez-vous optimiser, ne transpirez pas les petites choses. Maintenant, personne ne se soucie d'environ 4 kb de mémoire gaspillée, à moins qu'ils ne travaillent sur des systèmes embarqués. Lorsque vous arrivez à 1 Go d'objets entre 1Mb et 10 Mo chacun, le doublage est probablement trop trop (je veux dire, c'est-à-dire entre 100 et 1 000 objets). Si vous pouvez estimer le taux d'expansion attendu, vous pouvez le niveler à un taux de croissance linéaire à un moment donné. Si vous attendez environ 10 objets par minute, en croîtez à 5 à 10 tailles d'objets par étape (une fois toutes les 30 secondes à une minute) est probablement bien.

Ce que tout cela est tombé à l'it, ne le pensez pas, optimisez ce que vous pouvez et personnalisez-vous à votre application (et plate-forme) si vous le souhaitez.

0
Rybec Arethdar