web-dev-qa-db-fra.com

Pourquoi array.Push est-il parfois plus rapide que array [n] = value?

Comme résultat secondaire de tester du code, j'ai écrit une petite fonction pour comparer la vitesse d'utilisation de la méthode array.Push vs l'adressage direct (array [n] = value). À ma grande surprise, la méthode Push s'est souvent révélée plus rapide, en particulier dans Firefox et parfois dans Chrome. Par curiosité: quelqu'un a une explication? Vous pouvez trouver le test @ cette page (cliquez sur 'Comparaison des méthodes de tableau')

68
KooiInc

Toutes sortes de facteurs entrent en jeu, la plupart des implémentations JS utilisent une matrice plate qui se convertit en stockage fragmenté si cela devient nécessaire plus tard.

Fondamentalement, la décision de devenir clairsemée est une heuristique basée sur les éléments qui sont définis et la quantité d'espace qui serait gaspillée pour rester plate.

Dans votre cas, vous définissez d'abord le dernier élément, ce qui signifie que le moteur JS verra un tableau qui doit avoir une longueur de n mais seulement un seul élément. Si n est suffisamment grand, cela fera immédiatement du tableau un tableau clairsemé - dans la plupart des moteurs, cela signifie que toutes les insertions suivantes prendront le cas du tableau clairsemé lent.

Vous devez ajouter un test supplémentaire dans lequel vous remplissez le tableau de l'index 0 à l'index n-1 - cela devrait être beaucoup, beaucoup plus rapide.

En réponse à @Christoph et par désir de tergiverser, voici une description de la façon dont les tableaux sont (généralement) implémentés dans JS - les spécificités varient d'un moteur JS à un moteur JS mais le principe général est le même.

Tous les JS Objects (donc pas les chaînes, les nombres, true, false, undefined ou null) héritent d'un type d'objet de base - l'implémentation exacte varie, elle pourrait être Héritage C++, ou manuellement en C (il y a des avantages à le faire dans les deux sens) - le type d'objet de base définit les méthodes d'accès aux propriétés par défaut, par exemple.

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

Ce type d'objet gère toute la logique d'accès aux propriétés standard, la chaîne de prototypes, etc. Ensuite, l'implémentation du tableau devient

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

Maintenant, lorsque vous créez un tableau dans JS, le moteur crée quelque chose de semblable à la structure de données ci-dessus. Lorsque vous insérez un objet dans l'instance Array, la méthode put de Array vérifie si le nom de la propriété est un entier (ou peut être converti en entier, par exemple "121", "2341", etc.) entre 0 et 2 ^ 32 -1 (ou peut-être 2 ^ 31-1, j'oublie exactement). Si ce n'est pas le cas, la méthode put est transmise à l'implémentation de l'objet de base et la logique [[Put]] standard est exécutée. Sinon, la valeur est placée dans le propre stockage du tableau, si les données sont suffisamment compactes, le moteur utilisera le stockage de tableau plat, auquel cas l'insertion (et la récupération) n'est qu'une opération d'indexation de tableau standard, sinon le moteur convertira le tableau au stockage clairsemé, et mettre/obtenir utiliser une carte pour passer de propertyName à la valeur de l'emplacement.

Honnêtement, je ne sais pas si un moteur JS passe actuellement d'un stockage clairsemé à un stockage plat après cette conversion.

Quoi qu'il en soit, c'est un aperçu assez élevé de ce qui se passe et laisse de côté certains des détails les plus épineux, mais c'est le modèle d'implémentation général. Les détails de la façon dont le stockage supplémentaire et la façon dont le put/get sont distribués diffèrent d'un moteur à l'autre - mais c'est le plus clair que je puisse vraiment décrire la conception/mise en œuvre.

Un point d'ajout mineur, alors que la spécification ES fait référence à propertyName en tant que chaîne Les moteurs JS ont également tendance à se spécialiser dans les recherches entières, donc someObject[someInteger] ne convertira pas l'entier en chaîne si vous regardez un objet qui a des propriétés entières, par exemple. Types de tableau, de chaîne et DOM (NodeLists, etc.).

83
olliej

Voici le résultat que j'obtiens avec votre test

sur Safari:

  • Array.Push (n) 1000000 valeurs: 0,124 s
  • Tableau [n .. 0] = valeur (décroissante) 1000000 valeurs: 3,697 sec
  • Tableau [0 .. n] = valeur (ascendante) 1000000 valeurs: 0,073 s

sur FireFox:

  • Array.Push (n) 1000000 valeurs: 0,075 s
  • Tableau [n .. 0] = valeur (décroissante) 1000000 valeurs: 1,193 s
  • Tableau [0 .. n] = valeur (croissante) 1 000 000 valeurs: 0,055 s

sur IE7:

  • Array.Push (n) 1000000 valeurs: 2,828 s
  • Tableau [n .. 0] = valeur (décroissante) 1000000 valeurs: 1,141 s
  • Tableau [0 .. n] = valeur (croissante) 1 000 000 valeurs: 7,984 s

Selon votre test la méthode Push semble être meilleure sur IE7 (énorme différence), et puisque de l'autre navigateurs la différence est petite, il semble que ce soit la méthode Push vraiment la meilleure façon d'ajouter un élément à un tableau.

Mais j'ai créé un autre script de test simple pour vérifier quelle méthode est rapide pour ajouter des valeurs à un tableau, les résultats m'ont vraiment surpris, en utilisant Array.length semble être beaucoup plus rapide que d'utiliser Array.Push , donc je ne sais vraiment plus quoi dire ou penser, je ne sais rien.

BTW: sur mon IE7 votre script s'arrête et les navigateurs me demandent si je veux le laisser continuer (vous connaissez le message typique IE qui dit: "Arrêter l'exécution de ce script?. .. ") Je recommanderais de réduire un peu les boucles.

10
Marco Demaio

Push() est un cas particulier du plus général [[Put]] et peut donc être encore optimisé:

Lors de l'appel de [[Put]] sur un objet tableau, l'argument doit d'abord être converti en un entier non signé car tous les noms de propriété - y compris les indices de tableau - sont des chaînes. Elle doit ensuite être comparée à la propriété length du tableau afin de déterminer si la longueur doit être augmentée ou non. Lorsque vous poussez, aucune conversion ou comparaison de ce type ne doit avoir lieu: utilisez simplement la longueur actuelle comme index de tableau et augmentez-la.

Bien sûr, il y a d'autres choses qui affecteront le runtime, par exemple, appeler Push() devrait être plus lent que d'appeler [[Put]] via [] car la chaîne du prototype doit être vérifiée pour la première.


Comme l'a souligné olliej: les implémentations ECMAScript réelles optimiseront la conversion, c'est-à-dire pour les noms de propriétés numériques, aucune conversion de chaîne en uint n'est effectuée mais juste une simple vérification de type. L'hypothèse de base devrait être maintenue, bien que son impact soit moindre que je ne l'avais initialement supposé.

6
Christoph

Voici un bon banc d'essai, qui confirme que l'attribution directe est nettement plus rapide que Push: http://jsperf.com/array-direct-assignment-vs-Push .

Edit: il semble y avoir un problème dans l'affichage des données de résultats cumulatifs, mais j'espère que cela sera bientôt corrigé.

4
Timo Kähkönen