64 votes

Définir des méthodes par le biais d'un prototype ou les utiliser dans le constructeur : une différence de performance réelle ?

En JavaScript, nous avons deux façons de créer une "classe" et de lui donner des fonctions publiques.

Méthode 1 :

function MyClass() {
    var privateInstanceVariable = 'foo';
    this.myFunc = function() { alert(privateInstanceVariable ); }
}

Méthode 2 :

function MyClass() { }

MyClass.prototype.myFunc = function() { 
    alert("I can't use private instance variables. :("); 
}

J'ai lu de nombreuses fois que les gens en disant que l'utilisation de la méthode 2 est plus efficace car toutes les instances partagent la même copie de la fonction plutôt que d'avoir chacune la sienne. La définition de fonctions via le prototype présente toutefois un énorme inconvénient : elle rend impossible la création de variables d'instance privées.

Même si, en théorie, l'utilisation de la méthode 1 donne à chaque instance d'un objet sa propre copie de la fonction (et utilise donc beaucoup plus de mémoire, sans parler du temps nécessaire aux allocations), est-ce bien ce qui se passe en pratique ? Il semble qu'une optimisation que les navigateurs Web pourraient facilement réaliser consisterait à reconnaître ce modèle extrêmement courant et à faire en sorte que toutes les instances de l'objet fassent référence aux fonctions suivantes le même une copie des fonctions définies via ces "fonctions constructives". Il ne pourrait alors donner à une instance sa propre copie de la fonction que si celle-ci est explicitement modifiée par la suite.

N'importe quel point de vue - ou, encore mieux, l'expérience du monde réel - sur les différences de performance entre les deux, serait extrêmement utile.

0voto

jgmjgm Points 36

Cette réponse doit être considérée comme une expansion du reste des réponses, remplissant les points manquants. Elle intègre à la fois l'expérience personnelle et les critères de référence.

D'après mon expérience, j'utilise religieusement les constructeurs pour construire littéralement mes objets, que les méthodes soient privées ou non. La raison principale est que lorsque j'ai commencé, c'était l'approche la plus facile et immédiate pour moi, donc ce n'est pas une préférence spéciale. Cela aurait pu être aussi simple que le fait que j'aime l'encapsulation visible et que les prototypes sont un peu désincarnés. Mes méthodes privées seront également assignées comme variables dans la portée. Bien que ce soit mon habitude et que cela permette de garder les choses bien contenues, ce n'est pas toujours la meilleure habitude et je me heurte parfois à des murs. En dehors des scénarios farfelus avec un auto-assemblage hautement dynamique en fonction des objets de configuration et de la disposition du code, cette approche tend à être la plus faible à mon avis, en particulier si les performances sont une préoccupation. Savoir que les internes sont privés est utile mais vous pouvez y parvenir par d'autres moyens avec la bonne discipline. À moins que les performances ne soient une considération sérieuse, utilisez ce qui fonctionne le mieux pour la tâche à accomplir.

  1. L'utilisation de l'héritage des prototypes et d'une convention pour marquer les éléments comme privés facilite le débogage car vous pouvez alors parcourir le graphe d'objets facilement depuis la console ou le débogueur. D'un autre côté, une telle convention rend l'obscurcissement un peu plus difficile et permet à d'autres personnes de greffer plus facilement leurs propres scripts sur votre site. C'est l'une des raisons pour lesquelles l'approche de la portée privée a gagné en popularité. Il ne s'agit pas d'une véritable sécurité, mais plutôt d'une résistance supplémentaire. Malheureusement, beaucoup de gens pensent encore que c'est une façon authentique de programmer un JavaScript sécurisé. Depuis que les débogueurs sont devenus très performants, l'obfuscation du code prend sa place. Si vous êtes à la recherche de failles de sécurité où trop de choses reposent sur le client, c'est un modèle de conception que vous devriez rechercher.
  2. Une convention vous permet d'avoir des propriétés protégées sans trop de soucis. Cela peut être une bénédiction et une malédiction. Elle facilite certains problèmes d'héritage car elle est moins restrictive. Le risque de collision ou de charge cognitive accrue est toujours présent, car il faut envisager d'autres accès à une propriété. Les objets auto-assemblés vous permettent de faire des choses étranges où vous pouvez contourner un certain nombre de problèmes d'héritage, mais ils peuvent être non conventionnels. Mes modules ont tendance à avoir une structure interne riche où les choses ne sont pas retirées jusqu'à ce que la fonctionnalité soit nécessaire ailleurs (partagée) ou exposée à moins d'être nécessaire en externe. Le modèle de constructeur a tendance à conduire à la création de modules sophistiqués autonomes plutôt que de simples objets fragmentés. Si c'est ce que vous voulez, c'est parfait. Sinon, si vous voulez une structure et une disposition plus traditionnelles de la POO, je suggérerais probablement de réglementer l'accès par convention. Dans mes scénarios d'utilisation, la POO complexe n'est pas souvent justifiée et les modules font l'affaire.
  3. Tous les tests ici sont minimaux. Dans le monde réel, il est probable que les modules seront plus complexes, ce qui rendra l'impact beaucoup plus important que ce que les tests indiquent ici. Il est assez courant d'avoir une variable privée avec plusieurs méthodes travaillant sur elle et chacune de ces méthodes ajoutera plus de frais généraux sur l'initialisation que vous n'obtiendrez pas avec l'héritage de prototype. Dans la plupart des cas, cela n'a pas d'importance, car seules quelques instances de ces objets circulent, mais cumulativement, cela peut faire beaucoup.
  4. On suppose que les méthodes prototypes sont plus lentes à appeler en raison de la recherche de prototypes. Ce n'est pas une supposition injuste, j'ai moi-même fait la même chose jusqu'à ce que je la teste. En réalité, c'est complexe et certains tests suggèrent que cet aspect est trivial. Entre, prototype.m = f , this.m = f et this.m = function... ce dernier est nettement plus performant que les deux premiers, dont les performances sont à peu près identiques. Si la recherche de prototype était un problème important, les deux dernières fonctions seraient nettement plus performantes que les premières. Au lieu de cela, il se passe quelque chose d'étrange, du moins en ce qui concerne Canary. Il est possible que les fonctions soient optimisées en fonction de ce dont elles sont membres. Une multitude de considérations liées aux performances entrent en jeu. Il existe également des différences dans l'accès aux paramètres et aux variables.
  5. Capacité de la mémoire. Ce sujet n'est pas très bien traité ici. Une hypothèse que vous pouvez faire dès le départ et qui a de fortes chances d'être vraie est que l'héritage des prototypes est généralement beaucoup plus efficace en termes de mémoire et, d'après mes tests, c'est le cas en général. Lorsque vous construisez votre objet dans votre constructeur, vous pouvez supposer que chaque objet aura probablement sa propre instance de chaque fonction plutôt que d'être partagée, une carte de propriétés plus grande pour ses propriétés personnelles et probablement un surcoût pour garder la portée du constructeur ouverte également. Les fonctions qui opèrent sur la portée privée sont extrêmement et disproportionnellement gourmandes en mémoire. Je trouve que dans de nombreux scénarios, la différence proportionnelle en mémoire sera beaucoup plus significative que la différence proportionnelle en cycles CPU.
  6. Graphique de la mémoire. Vous pouvez également bloquer le moteur en rendant la GC plus coûteuse. Les profileurs ont tendance à montrer le temps passé en GC de nos jours. Ce n'est pas seulement un problème lorsqu'il s'agit d'allouer et de libérer davantage. Vous créez également un plus grand graphe d'objets à parcourir et d'autres choses de ce genre, de sorte que la GC consomme plus de cycles. Si vous créez un million d'objets et que vous y touchez à peine, selon le moteur, l'impact sur les performances ambiantes peut être plus important que prévu. J'ai prouvé que cela fait au moins fonctionner le GC plus longtemps lorsque les objets sont éliminés. C'est-à-dire qu'il tend à y avoir une corrélation entre la mémoire utilisée et le temps nécessaire à la GC. Cependant, dans certains cas, le temps est le même quelle que soit la mémoire utilisée. Cela indique que la composition du graphe (couches d'indirection, nombre d'éléments, etc.) a plus d'impact. Ce n'est pas quelque chose qui est toujours facile à prédire.
  7. Peu de gens utilisent les prototypes chaînés de manière intensive, moi y compris, je dois l'admettre. Les chaînes de prototypes peuvent être coûteuses en théorie. Quelqu'un le fera mais je n'ai pas mesuré le coût. Si, au lieu de cela, vous construisez vos objets entièrement dans le constructeur et qu'ensuite vous avez une chaîne d'héritage où chaque constructeur appelle un constructeur parent sur lui-même, en théorie l'accès aux méthodes devrait être beaucoup plus rapide. D'un autre côté, vous pouvez accomplir l'équivalent si cela est important (comme aplatir les prototypes vers le bas de la chaîne des ancêtres) et cela ne vous dérange pas de casser des choses comme hasOwnProperty, peut-être instanceof, etc. si vous en avez vraiment besoin. Dans un cas comme dans l'autre, les choses commencent à devenir complexes une fois que l'on s'est engagé dans cette voie, lorsqu'il s'agit d'améliorer les performances. Vous finirez probablement par faire des choses que vous ne devriez pas faire.
  8. De nombreuses personnes n'utilisent pas directement les deux approches que vous avez présentées. Au lieu de cela, ils font leurs propres choses en utilisant des objets anonymes permettant le partage de méthodes de n'importe quelle manière (mixins par exemple). Il existe également un certain nombre de frameworks qui mettent en œuvre leurs propres stratégies d'organisation des modules et des objets. Il s'agit d'approches personnalisées fortement basées sur des conventions. Pour la plupart des gens et pour vous, votre premier défi devrait être l'organisation plutôt que la performance. C'est souvent compliqué dans la mesure où Javascript offre de nombreuses façons de réaliser des choses par rapport à des langages ou des plateformes avec un support plus explicite de la POO/espace de nommage/module. En ce qui concerne les performances, je dirais plutôt qu'il faut avant tout éviter les pièges majeurs.
  9. Il y a un nouveau type de symbole qui est censé fonctionner pour les variables et méthodes privées. Il existe un certain nombre de façons de l'utiliser et cela soulève une foule de questions liées aux performances et à l'accès. Dans mes tests, les performances des symboles n'étaient pas très bonnes par rapport à tout le reste, mais je ne les ai jamais testés en profondeur.

Avis de non-responsabilité :

  1. Il y a beaucoup de discussions sur les performances et il n'y a pas toujours une réponse correcte permanente à ce sujet, car les scénarios d'utilisation et les moteurs changent. Toujours établir un profil, mais aussi toujours mesurer de plus d'une façon, car les profils ne sont pas toujours précis ou fiables. Évitez de consacrer des efforts importants à l'optimisation, à moins qu'il n'y ait un problème manifeste.
  2. Il est probablement préférable d'inclure des contrôles de performance pour les zones sensibles dans les tests automatisés et de les exécuter lorsque les navigateurs sont mis à jour.
  3. N'oubliez pas que la durée de vie de la batterie compte parfois autant que les performances perceptibles. La solution la plus lente peut s'avérer plus rapide après l'utilisation d'un compilateur optimisant (par exemple, un compilateur peut avoir une meilleure idée du moment où l'on accède à des variables à portée restreinte qu'à des propriétés marquées comme privées par convention). Envisagez un backend tel que node.js. Cela peut nécessiter une meilleure latence et un meilleur débit que ce que l'on trouve souvent sur le navigateur. La plupart des gens n'ont pas besoin de s'inquiéter de ces choses avec quelque chose comme la validation d'un formulaire d'inscription, mais le nombre de scénarios divers où ces choses peuvent avoir de l'importance augmente.
  4. Il faut être prudent avec les outils de suivi de l'allocation de la mémoire pour conserver le résultat. Dans certains cas, lorsque je n'ai pas retourné et persisté les données, celles-ci ont été entièrement optimisées ou le taux d'échantillonnage n'était pas suffisant entre l'instanciation et la non-référence, ce qui m'a laissé perplexe quant à la façon dont un tableau initialisé et rempli à un million d'exemplaires a été enregistré comme 3,4KiB dans le profil d'allocation.
  5. Dans le monde réel, dans la plupart des cas, la seule façon de vraiment optimiser une application est de l'écrire en premier lieu afin de pouvoir la mesurer. Des dizaines, voire des centaines de facteurs peuvent entrer en jeu, voire des milliers, dans un scénario donné. Les moteurs font également des choses qui peuvent conduire à des caractéristiques de performance asymétriques ou non linéaires. Si vous définissez des fonctions dans un constructeur, elles peuvent être des fonctions flèches ou traditionnelles, chacune se comporte différemment dans certaines situations et je n'ai aucune idée des autres types de fonctions. Les classes ne se comportent pas non plus de la même manière en termes de performances pour les constructeurs prototypés qui devraient être équivalents. Vous devez également être très prudent avec les benchmarks. Les classes prototypées peuvent avoir une initialisation différée de diverses manières, surtout si vous avez également prototypé vos propriétés (conseil, ne le faites pas). Cela signifie que vous pouvez sous-estimer le coût d'initialisation et surestimer le coût d'accès/de mutation des propriétés. J'ai également vu des indications d'optimisation progressive. Dans ces cas, j'ai rempli un grand tableau avec des instances d'objets identiques et, à mesure que le nombre d'instances augmente, les objets semblent être progressivement optimisés pour la mémoire jusqu'à un point où le reste est identique. Il est également possible que ces optimisations aient un impact significatif sur les performances du CPU. Ces éléments dépendent fortement non seulement du code que vous écrivez, mais aussi de ce qui se passe au moment de l'exécution, comme le nombre d'objets, la variance entre les objets, etc.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X