web-dev-qa-db-fra.com

Performances: Immutable.js Map vs List vs plain JS

Question

Y a-t-il un problème avec mon indice de référence? Comment Immutable.js find () peut-il être 8 fois plus lent que array.find ()?

Ok, pas tout à fait juste, car j'utilise Immutable.Map à l'intérieur de Immutable.List. Mais pour moi, c'est un exemple du monde réel. Si j'utilise Immutable.js, c'est pour protéger l'immuabilité et gagner en performance dans certains aspects (où le partage structurel entre en jeu). Il serait inutile d'utiliser Immutable.js uniquement à la racine de l'objet.

Le repère ci-dessous est en fait de ne autre question (le mien aussi). J'ai été tellement surpris par les résultats, j'ai dû le poster séparément pour être clair. Ai-je fait quelque chose de mal dans mes benchmarks, ou la différence de performances est-elle vraiment si grande?

Contexte

Certaines des données de mon application peuvent être considérées comme des métadonnées d'application. Les données d'origine vivent dans une base de données sur le serveur. Les mises à jour des métadonnées ne seront pas effectuées souvent. L'application vérifiera les métadonnées mises à jour au démarrage.

J'utilise Immutable.js partout, mais je reviendrai sur plain js pour les métadonnées. Aucun partage structurel sophistiqué n'est nécessaire pour ce type de données.

Le test consiste à trouver des valeurs par clé dans une collection

  • Collection de 10 pièces

  • Trouvez une valeur un million de fois

  • Mac mini core i7 2.6

Résultat:

  • Objet JS simple avec clés forcées: 8 ms

  • Tableau JS simple utilisant find (): 127 ms

  • Carte immuable avec touches numériques: 185 ms

  • Immuable.Liste utilisant find (): 972 ms !! je suis déconcerté

Comme j'utilise React Native, je dois toujours faire attention à la limite de 16 ms si je veux atteindre 60 fps. Les valeurs de référence ne semblent pas être linéaires. Exécuter le test avec uniquement 100 recherches prennent 1 ms avec Map et 2 ms avec List, ce qui est assez cher.

Code de test

let Immutable = require('immutable');

let mapTest = Immutable.Map()
  .set(1, Immutable.Map({value: 'one'}))
  .set(2, Immutable.Map({value: 'two'}))
  .set(3, Immutable.Map({value: 'three'}))
  .set(4, Immutable.Map({value: 'four'}))
  .set(5, Immutable.Map({value: 'five'}))
  .set(6, Immutable.Map({value: 'six'}))
  .set(7, Immutable.Map({value: 'seven'}))
  .set(8, Immutable.Map({value: 'eight'}))
  .set(9, Immutable.Map({value: 'nine'}))
  .set(10, Immutable.Map({value: 'ten'}));

let listTest = Immutable.fromJS([
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
])

let objTest = {
  1:  {value: 'one'},
  2:  {value: 'two'},
  3:  {value: 'three'},
  4:  {value: 'four'},
  5:  {value: 'five'},
  6:  {value: 'six'},
  7:  {value: 'seven'},
  8:  {value: 'eight'},
  9:  {value: 'nine'},
  10: {value: 'ten'}
};

let arrayTest = [
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
];

const runs = 1e6;
let i;
let key;
let hrStart;

console.log(' ')
console.log('mapTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = mapTest.getIn([key, 'value'] )
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('listTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = listTest
    .find(item => item.get('key') === key)
    .get('value');
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);

console.log(' ')
console.log('arrayTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = arrayTest
    .find(item => item.key === key)
    .value

  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('objTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = objTest[key].value
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
28
Michael

La réponse courte est que la représentation des structures de données utilisées par Immutable.js nécessite beaucoup de surcharge supplémentaire pour parcourir les éléments d'une liste, par rapport à un tableau JS natif.

Analyse comparative Immutable.List.find et Array.find

Votre référence est bonne, mais nous pouvons simplifier un peu les choses en nous débarrassant de la carte imbriquée; vous avez raison de considérer les performances pour des problèmes réalistes, mais il peut être utile de comprendre les différences de performances pour simplifier le problème autant que possible. Il est également souvent utile pour comparer les performances des différentes tailles d'entrée. Par exemple, il est possible que dans Immutable.js, List.prototype.find est implémenté de telle manière que l'appel initial et la configuration prennent un certain temps, mais que l'itération suivante dans la liste fonctionne de la même façon que les tableaux JS natifs; dans ce cas, la différence de performances entre les tableaux JS natifs et les listes Immutable.js diminuerait pour les longues longueurs d'entrée (cela ne s'avère pas être le cas).

Créons également notre propre fonction de recherche pour les tableaux JS natifs, Array.prototype.ourFind à comparer au natif Array.prototype.find pour déterminer si la différence pourrait être due en partie aux performances des fonctions JS elles-mêmes par rapport aux performances des fonctions intégrées à l'implémentation.

Array.prototype.ourFind = function(predicate) {
  for (let i = 0; i < this.length; i++) {
    if (predicate(this[i])) return this[i];
  }
}

function arrayRange(len) {
  return new Array(len).fill(null).map((_, i) => i);
}

function immutListRange(len) {
  return Immutable.fromJS(arrayRange(len));
}

function timeFind(coll, find, iters) {
  let startTime = performance.now();
  for (let i = 0; i < iters; i++) {
    let searchVal = i % coll.length,
      result = find.call(coll, item => item === searchVal);
  }
  return Math.floor(performance.now() - startTime);
}

const MIN_LEN = 10,
  MAX_LEN = 1e4,
  ITERS = 1e5;

console.log('\t\tArray.find\tArray.ourFind\tList.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
  console.log(`${len}\t\t\t` +
    `${timeFind(arrayRange(len), Array.prototype.find, ITERS)}\t\t\t` +
    `${timeFind(arrayRange(len), Array.prototype.ourFind, ITERS)}\t\t\t` +
    `${timeFind(immutListRange(len), Immutable.List.prototype.find, ITERS)}`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>

Dans Chrome, j'obtiens:

Length .    Array.find  Array.ourFind   List.find
10          28          13              96
100         60          44              342
1000        549         342             3016
10000       5533        3142            36423

J'ai obtenu des résultats à peu près similaires dans Firefox et Safari. Quelques points à noter:

  1. La différence entre List.find contre. Array.find n'est pas simplement dû aux implémentations natives (c'est-à-dire intégrées à l'interpréteur) par rapport aux implémentations JS, car une implémentation JS de Array.ourFind fonctionne au moins aussi bien que Array.find.
  2. Toutes les implémentations fonctionnent en O(n) temps (c.-à-d. Que le temps d'exécution est linéaire par rapport à la longueur d'entrée). Ceci est normal, car un algorithme de recherche devra toujours fonctionner en itérant à travers le éléments de collection jusqu'à ce qu'il en trouve un pour lequel le prédicat renvoie true.
  3. Immutable.List.find est ~ 6 fois plus lent que Array.find, conformément à vos résultats d'analyse comparative.

Représentation immuable des données de la liste

Pour comprendre pourquoi Immutable.List.find est tellement plus lent, vous devez d'abord considérer comment Immutable.List représente le contenu de la liste.

Un moyen rapide de le faire consiste à générer un Immutable.List et examinez-le dans la console:

console.log(immutListRange(1000));  // immutListRange defined above

Donc, il ressemble essentiellement à Immutable.List représente le contenu sous forme d'arbre avec un facteur de ramification de 32.

Considérez maintenant ce qu'il faut pour exécuter une opération de recherche sur les données qui sont représentées de cette manière. Vous devrez commencer au nœud racine et parcourir l'arborescence jusqu'au premier nœud feuille (qui contient un tableau avec les données réelles) et parcourir le contenu de la feuille; si l'élément n'est pas trouvé, vous devez aller au prochain nœud feuille et rechercher ce tableau, et ainsi de suite. C'est une opération plus complexe que la simple recherche dans un seul tableau, et elle nécessite une surcharge pour s'exécuter.

Regarder Immutable.List.find au travail

Une excellente façon d'apprécier le travail que Immutable.List.find ne consiste à définir un point d'arrêt dans le débogueur de votre choix et à parcourir l'opération. Vous verrez que Immutable.List.Find n'est pas une opération aussi simple que la simple boucle sur un seul tableau.

Commentaires supplémentaires

La représentation arborescente des données dans Immutable.js accélère vraisemblablement d'autres opérations, mais entraîne une pénalité de performance avec certaines fonctions, telles que find.

En remarque, je ne pense pas dans la plupart des cas que le choix d'utiliser des structures de données immuables soit motivé par des considérations de performances. Il peut y avoir des cas où les structures de données immuables fonctionnent mieux que les structures mutables (et certainement les structures de données immuables rendent le calcul parallèle moins complexe, ce qui permet un gain de performances significatif), mais il y aura de nombreux cas où l'inverse est vrai. Au contraire, le choix de l'immuabilité est, dans la plupart des cas, dicté par des considérations de conception - c'est-à-dire. l'utilisation de structures de données immuables oblige les conceptions de programmes à être plus robustes et, à long terme, à augmenter la productivité des développeurs.

22
cjg

Les moteurs JS sont très bons pour optimiser les opérations "à chaud" - celles qui se répètent souvent et qui sont aussi simples que possible (par exemple TurboFan dans V8 ). Les objets JS simples et les fonctions de tableau vont toujours battre une bibliothèque comme Immutable.js, où List appels Collection appels Seq appels Operations (et ainsi de suite), en particulier lorsque les actions sont répétées plusieurs fois.

Immutable.js semble être conçu pour être pratique à utiliser et éviter une grande partie de la méchanceté des collections JS mutables, plutôt que des performances pures.

Si vous avez un million de choses, utilisez un objet ou un tableau JS de bas niveau (ou Web Assembly, si les performances sont critiques). Si vous avez mille choses et que avez besoin d'être certain de ne pas laisser tomber un cadre, alors le JS simple est toujours le chemin à parcourir. Ce sont des cas spécialisés - pour la plupart des cas d'utilisation, la commodité d'Immutable.js vaut la réduction de vitesse.

11
Keith

Le benchmark ne prend pas en compte tous les types de données qu'Immutable peut offrir. Immutable a en fait certaines fonctionnalités, que les objets/tableaux simples n'ont pas: OrderedSet et OrderedMap ont les avantages des tableaux/listes indexés et des structures basées sur des clés comme objet/Map.

Vous trouverez ci-dessous une version adaptée du test bien fait de @Keith, qui montre que nous pouvons réellement devenir plus rapides que Array.find, en particulier avec de grands ensembles de données.

Bien sûr, cela a aussi un coût:

  • Set/Map n'autorise pas les doublons (pas différent de l'objet cependant).
  • Dans les coulisses, les variantes ordonnées combinent une carte/ensemble avec une liste, ce qui consomme plus de mémoire.

Notez que OrderedSet est plus cher que Set non ordonné et peut consommer plus de mémoire. OrderedSet # add est amorti O (log32 N), mais pas stable.

function arrayFind(coll, searchVal) {
  return coll.find(item => item === searchVal);
}

function immutableSetGet(coll, searchVal) {
  return coll.get(searchVal);
}

function arrayRange(len) {
  return new Array(len).fill(null).map((_, i) => i);
}

function immutOrderedSetRange(len) {
  return Immutable.OrderedSet(arrayRange(len));
}

function timeFind(what, coll, find, iters) {
  let startTime = performance.now();
  let size = coll.length || coll.size;
  for (let i = 0; i < iters; i++) {
    let searchVal = i % size,
      result = find(coll, searchVal);
  }
  return Math.floor(performance.now() - startTime);
}

const MIN_LEN = 100,
  MAX_LEN = 1e4,
  ITERS = 50000;

console.log('\t\t\tArray.find\tOrderedSet.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
  console.log(`${len}\t\t\t` +
    `${timeFind('find', arrayRange(len), arrayFind, ITERS)}\t\t` +
    `${timeFind('set', immutOrderedSetRange(len), immutableSetGet, ITERS)}`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
1
dube