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.
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);
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.
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:
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
.Immutable.List.find
est ~ 6 fois plus lent que Array.find
, conformément à vos résultats d'analyse comparative.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.
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.
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.
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.
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:
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>