web-dev-qa-db-fra.com

Pourquoi <= plus lent que <utilise-t-il cet extrait de code dans la V8?

Je lis les diapositives Briser la limite de vitesse Javascript avec V8 , et voici un exemple comme le code ci-dessous. Je n'arrive pas à comprendre pourquoi <= est plus lent que < dans ce cas, quelqu'un peut-il expliquer cela? Tous les commentaires sont appréciés.

Lent:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(Indice: prime est un tableau de longueur premier_compte)

Plus rapide:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[Plus d'infos] L’amélioration de la vitesse est importante, dans mon test d’environnement local, les résultats sont les suivants:

V8 version 7.3.0 (candidate) 

Lent:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

Plus rapide:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed
163
Leonardo Physh

Je travaille sur V8 chez Google et je voulais apporter quelques informations supplémentaires en plus des réponses et des commentaires existants.

Pour référence, voici l'exemple de code complet de les diapositives :

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

Tout d’abord, la différence de performances n’a rien à voir avec les opérateurs < et <= directement. Alors s'il vous plaît, ne sautez pas dans les cerceaux juste pour éviter <= dans votre code car vous avez lu sur Stack Overflow qu'il est lent - ce n'est pas le cas!


Deuxièmement, les gens ont souligné que le tableau est "troué". Cela ne ressortait pas clairement de l'extrait de code dans la publication d'OP, mais cela apparaît clairement lorsque vous regardez le code qui initialise this.primes:

this.primes = new Array(iterations);

Il en résulte un tableau avec type HOLEY éléments dans V8, même si le tableau finit par être complètement rempli/compacté/contigu. En général, les opérations sur des tableaux à trous sont plus lentes que celles sur des tableaux à garnissage, mais dans ce cas, la différence est négligeable: elle équivaut à 1 Smi supplémentaire (), un petit entier . contre les trous) à chaque fois que nous frappons this.primes[i] dans la boucle dans isPrimeDivisible. Pas grave!

TL; DR Le tableau étant HOLEY n’est pas le problème ici.


D'autres ont souligné que le code se lisait hors limites. Il est généralement recommandé de éviter de lire au-delà de la longueur des tableaux , et dans ce cas, cela aurait effectivement permis d'éviter la chute massive des performances. Mais pourquoi? La V8 peut gérer certains de ces scénarios hors limites avec un impact mineur sur les performances. Quelle est la spécificité de ce cas particulier, alors?

La lecture hors limites a pour résultat que this.primes[i] est undefined sur cette ligne:

if ((candidate % this.primes[i]) == 0) return true;

Et cela nous amène à le vrai problème : l'opérateur % est maintenant utilisé avec des opérandes non entiers!

  • integer % someOtherInteger peut être calculé très efficacement; Les moteurs JavaScript peuvent produire un code machine hautement optimisé pour ce cas.

  • integer % undefined par contre, représente un moyen moins efficace Float64Mod, puisque undefined est représenté sous forme de double.

L’extrait de code peut en effet être amélioré en remplaçant le <= en < sur cette ligne:

for (var i = 1; i <= this.prime_count; ++i) {

... pas parce que <= est en quelque sorte un opérateur supérieur à <, mais simplement parce que cela évite les valeurs hors limites lues dans ce cas particulier.

131
Mathias Bynens

D'autres réponses et commentaires mentionnent que la différence entre les deux boucles réside dans le fait que la première exécute une itération de plus que la seconde. Cela est vrai, mais dans un tableau de 25 000 éléments, une itération plus ou moins ne ferait qu'une différence minime. Comme on peut le deviner, si nous supposons que la longueur moyenne à la croissance est de 12 500, alors la différence que nous pourrions espérer devrait être d’environ 1/12 500, soit seulement 0,008%.

La différence de performances ici est beaucoup plus grande que ce qui serait expliqué par cette itération supplémentaire, et le problème est expliqué vers la fin de la présentation.

this.primes est un tableau contigu (chaque élément contient une valeur) et les éléments sont tous des nombres.

Un moteur JavaScript peut optimiser un tel tableau pour qu’il soit un simple tableau de nombres réels, au lieu d’un tableau d’objets contenant des nombres, mais pouvant contenir d’autres valeurs ou aucune valeur. L'accès au premier format est beaucoup plus rapide: il nécessite moins de code et le tableau est beaucoup plus petit, ce qui lui permet de mieux s'intégrer dans le cache. Cependant, certaines conditions peuvent empêcher l'utilisation de ce format optimisé.

Une condition serait que certains éléments du tableau manquent. Par exemple:

let array = [];
a[0] = 10;
a[2] = 20;

Maintenant, quelle est la valeur de a[1]? Il n'a pas de valeur . (Il n'est même pas correct de dire qu'il a la valeur undefined - un élément de tableau contenant la valeur undefined est différent d'un élément de tableau totalement manquant.)

Il n'y a pas moyen de représenter cela uniquement avec des chiffres, le moteur JavaScript est donc obligé d'utiliser le format le moins optimisé. Si a[1] contenait une valeur numérique comme les deux autres éléments, le tableau pourrait éventuellement être optimisé en un tableau de nombres uniquement.

Une autre raison pour laquelle un tableau est forcé dans le format désoptimisé peut être si vous tentez d'accéder à un élément en dehors des limites du tableau, comme indiqué dans la présentation.

La première boucle avec <= tente de lire un élément au-delà de la fin du tableau. L'algorithme fonctionne toujours correctement, car dans la dernière itération supplémentaire:

  • this.primes[i] est évalué à undefined parce que i est passé la fin du tableau.
  • candidate % undefined (pour toute valeur de candidate) est évalué à NaN.
  • NaN == 0 est évalué à false.
  • Par conséquent, le return true n'est pas exécuté.

Donc, c'est comme si l'itération supplémentaire ne s'était jamais produite - cela n'a aucun effet sur le reste de la logique. Le code produit le même résultat qu'il le ferait sans itération supplémentaire.

Mais pour y arriver, il a essayé de lire un élément inexistant au-delà de la fin du tableau. Cela force le tableau à ne plus être optimisé - ou du moins l’a fait au moment de cette discussion.

La deuxième boucle avec < ne lit que les éléments existant dans le tableau, ce qui permet d'optimiser le tableau et le code.

Le problème est décrit dans pages 90-91 de la présentation, avec une discussion dans les pages qui précèdent et suivent.

Il m'est arrivé d'assister à cette présentation Google I/O et de parler ensuite à l'orateur (l'un des auteurs de V8). J'avais utilisé une technique dans mon propre code qui consistait à lire au-delà de la fin d'un tableau comme une tentative malavisée (avec le recul) d'optimiser une situation particulière. Il a confirmé que si vous essayiez de lire même au-delà de la fin d'un tableau, cela empêcherait l'utilisation du format optimisé simple.

Si ce que l'auteur de la V8 a dit est toujours vrai, la lecture au-delà de la fin de la matrice l'empêcherait d'être optimisée et il faudrait revenir au format plus lent.

Maintenant, il est possible que la V8 ait été améliorée entre-temps pour traiter efficacement ce cas, ou que d'autres moteurs JavaScript le traitent différemment. Je ne connais ni l’un ni l’autre, mais c’est cette déoptimisation dont parlait l’exposé.

226
Michael Geary

Pour ajouter un peu de caractère scientifique, voici un jsperf

https://jsperf.com/ints-values-in-out-of-array-bounds

Il teste le cas de contrôle d'un tableau rempli d'ints et de boucles faisant de l'arithmétique modulaire tout en restant dans les limites. Il a 5 cas de test:

  • 1. Boucle hors limites
  • 2. Tableaux troués
  • 3. Arithmétique modulaire contre NaN
  • 4. Valeurs complètement indéfinies
  • 5. Utiliser une new Array()

Cela montre que les 4 premiers cas sont vraiment mauvais pour la performance. La boucle en dehors des limites est un peu meilleure que les 3 autres, mais les 4 sont à peu près 98% plus lentes que dans le meilleur des cas.
La casse new Array() est presque aussi bonne que la matrice brute, seulement quelques pour cent plus lente.

6
Nathan Adams