À propos des méthodes mutables d'un objet Math en JavaScript

Aujourd'hui, nous publions la traduction d'un article sur les calculs mathématiques en JavaScript, qui est une version écrite de la présentation de son auteur sur WaffleJS . Et cette déclaration elle-même était un peu la continuation de cette conversation sur Twitter.


Enseignement des mathématiques

Intérêt pour les mathématiques


Tout a commencé avec mon éducation mathématique, dont la réception a été quelque peu retardée. Quand j'ai étudié l'informatique, je pouvais communiquer avec des professeurs de mathématiques de classe mondiale, mais j'ai raté cette occasion. Je n'aimais pas les mathématiques: les sujets étudiés étaient très loin de la pratique. De plus, j'étais déjà déséquilibré par un programme de formation informatique profondément théorique. J'ai alors cru qu'elle était divorcée de la vie et, pour la plupart, je continue de le penser.

Mais maintenant, quelques années après avoir terminé mes études, une soif d'apprendre les mathématiques s'est réveillée en moi. J'ai été inspiré par l'effet que même une petite application des connaissances mathématiques peut avoir sur mon travail et sur mon hobby. Mais je n'avais pas de plan d'étude clair.

Puis, en 2012, j'ai trouvé un moyen d'étudier les mathématiques en commençant à travailler sur le projet Simple Statistics . Depuis lors, j'ai développé et soutenu ce projet. Maintenant, il inclut des implémentations de nombreux algorithmes, il s'est avéré être l'une des bibliothèques mathématiques mathématiques les plus « étoilées ». Et, apparemment, les gens utilisent vraiment cette bibliothèque.

Mais j'ai commencé à travailler en 2012. Si nous parlons de l'évolution de la technologie au cours de cette période, c'était il y a très, très longtemps. Depuis lors, 8 versions LTS de Node.js ont été publiées . Depuis lors, JavaSript lui-même et les environnements dans lesquels les programmes écrits dans ce langage fonctionnent ont beaucoup changé. En 2012, la bibliothèque React n'existait pas encore, alors le premier commit sur le projet Babel n'était pas encore fait.


Le passage du temps

Au fil des ans, j'ai remarqué que mes tests se sont écrasés lors de la mise à jour de Node.js. Par exemple, je pourrais avoir quelque chose comme ce test:

t.equal(ss.gamma(11.54), 13098426.039156161);

Ce test fonctionne correctement dans Node.js v10, mais se casse dans Node.js v12. Et ici, ce n'est pas une méthode super compliquée qui est testée: la fonction est gammaimplémentée à l'aide de fonctions JavaScript standard - Math.pow, Math.sqrtet Math.sin.

Arithmétique


Je sais à quoi vous pouvez penser ici: l'arithmétique. Sur Twitter, des discussions animées éclatent périodiquement en raison des résultats du calcul de l'expression suivante:

0.1 + 0.2 = 0.30000000000000004

Mais, comme je l'ai déjà écrit , tous les langages de programmation populaires se comportent de cette façon, même les plus démodés et pédants comme Haskell. L'arithmétique en virgule flottante peut sembler étrange, mais ils se comportent de la même manière, leur comportement est bien documenté. A savoir, nous parlons de la norme IEEE 754 , dont les exigences sont strictement implémentées dans les langages de programmation. Le problème n'est donc pas arithmétique: la mise en œuvre de l'addition, de la soustraction, de la division et de la multiplication dans les langages, la programmation peut être dite «gravée dans la pierre».

Objet mathématique


Mon problème était dans l'objet JavaScript standard Math. En particulier, dans toutes les méthodes de cet objet.

Des techniques telles que Math.sin, Math.cos, Math.exp, Math.pow, Math.tan- sont les ingrédients de base pour géométriques calculs et autres. Quand j'ai compris cela, j'ai commencé à étudier séparément les changements dans le comportement des méthodes de base de l'objet Mathdans différentes versions de Node.js. Voici quelques exemples.

Calcul Math.tanh(0.1):

// Node 4
0.09966799462495590234
// Node 6
0.09966799462495581907

Calcul Math.pow(1/3, 3):

// Node 10
0.03703703703703703498
// Node 12
0.03703703703703702804

Pire encore, ce problème n'est pas seulement apparent dans Node.js. La même chose se produit dans les navigateurs et dans d'autres environnements qui prennent en charge JavaScript.

Cela nous amène à la question suivante: qu'est-ce que le calcul mathématique?


Représentation graphique des calculs

Les méthodes trigonométriques sont faciles à visualiser. Si vous avez un seul cercle et plusieurs mois de lycée à votre disposition, alors vous savez que le cosinus et le sinus sont les coordonnées d'un certain point sur le bord du cercle, et que les graphiques des fonctions sin et cos ressemblent à des vagues. En fait, au lycée, ils étudient la dérivation de ces valeurs, mais la méthode utilisée pour cela - la série Taylor - repose sur une série infinie, et il n'est pas facile pour un ordinateur de résoudre de tels problèmes.

Voici ce que vous pouvez apprendre de Wikipediaconcernant l'algorithme de calcul du sinus: «Il n'y a pas d'algorithme standard pour calculer le sinus. IEEE 754-2008, la norme la plus utilisée pour les calculs en virgule flottante, n'affecte pas le calcul des fonctions trigonométriques comme un sinus. "

Les ordinateurs utilisent de nombreuses approximations et algorithmes différents pour effectuer des calculs, quelque chose comme CORDIC , toutes sortes de trucs délicats et de tables de recherche. Toute cette hétérogénéité explique la présence de nombreuses bibliothèques fastmath sur GitHub . Le fait est qu'il existe de nombreuses façons de mettre en œuvre la méthode Math.sin. Et d'autres fonctions aussi. Par exemple, comme vous le savez, Quake III Arena utilisé un plus rapideremplacement de la méthode standard de calcul de la racine carrée inverse pour accélérer le rendu.

En conséquence, les calculs mathématiques sont le résultat de la mise en œuvre de certains algorithmes. En pratique, de nombreux algorithmes courants et leurs variantes sont utilisés.

La spécification JavaScript, au lieu de spécifier l'algorithme particulier à utiliser dans les implémentations de langage, donne aux implémentations une grande marge de manœuvre en termes de fonctions utilisées dans les calculs mathématiques.

Voici ce que dit la norme à ce sujet (ECMA-262, 10 édition, section 20.2.2):

"Le comportement des fonctions acos, acosh, asin, asinh, atan, atanh, atan2, cbrt, cos, cosh, exp, expm1, hypot, log, log1p, log2, log10, pow, random, sin, sinh, sqrt, tan et tanh il n'est pas entièrement décrit ici, à l'exception des exigences concernant le retour de certains résultats pour des valeurs spécifiques des arguments, qui sont des cas limites notables. "

Je ne sais pas comment fonctionnent les activités internes des membres du comité responsable de la norme ECMA-262, mais je pense qu'ils ont élaboré la norme afin qu'il n'y ait pas de crise de compatibilité dans JavaScript si Intel ou AMD émettaient de nouvelles instructions mathématiques ultrarapides dans leurs nouveaux processeurs.

En raison du fait qu'il existe de nombreux interpréteurs JavaScript largement utilisés, du fait que JavaScript est souvent utilisé dans les navigateurs, et entre les navigateurs, il y a toujours quelque chose comme la concurrence, et du fait que même les implémentations JavaScript populaires sont sous pression constante et forcé d'évoluer rapidement, offrant les meilleures performances ... à cause de tout cela, nous avons ce que nous avons. Quiconque utilise JavaScript rencontrera régulièrement le fait que dans différentes implémentations les résultats des calculs mathématiques effectués au moyen de l'objet Mathsont différents.

Ce n'est pas aussi significatif dans d'autres langages interprétés, car ils ont généralement des implémentations «canoniques». Par exemple, cela est vrai pour l'interpréteur Python.

Où sont effectués les calculs?


Examinons maintenant de plus près où les calculs sont effectués. Dans le cas de JavaScript, il existe trois domaines dans lesquels les calculs mathématiques de base sont effectués:

  1. CPU.
  2. Interpréteur de langage (code C ++ et C d'une implémentation JavaScript spécifique).
  3. Code JavaScript, par exemple, code pour les bibliothèques spécialisées.

▍1. CPU


La première idée qui m'est venue à l'esprit lorsque j'ai pensé aux endroits où les calculs sont effectués était que les calculs sont effectués dans le processeur. J'ai suggéré que puisque les processeurs implémentent des calculs arithmétiques, ils peuvent également implémenter des calculs plus complexes. Il s'est avéré que les processeurs ont des instructions pour effectuer des calculs trigonométriques et autres, mais ces instructions sont rarement utilisées. Par exemple, l'implémentation du calcul du sinus dans les processeurs avec une architecture x86 n'était pas très populaire, car cette implémentation ne s'avère pas nécessairement plus rapide que les implémentations logicielles (celles qui utilisent des opérations arithmétiques de processeur). De plus, il n'est pas nécessairement plus précis que les implémentations logicielles.

Intel a également subi une honte en raison de la très fortesurestimation de la précision des opérations trigonométriques dans la documentation. De telles erreurs sont particulièrement tragiques du fait qu'une puce, contrairement à un programme, ne peut pas être corrigée.

▍2. Interprète de langue


C'est ainsi que les sous-systèmes informatiques fonctionnent dans la plupart des implémentations JavaScript. Ils implémentent ces sous-systèmes de différentes manières.

  • Les moteurs V8 et SpiderMonkey utilisent des ports de bibliothèque fdlibm pour les calculs , qui sont légèrement différents. Cette bibliothèque, écrite à l'origine chez Sun Microsystems, a été transmise de génération en génération.
  • JavaScriptCore (Safari) utilise la bibliothèque cmath pour effectuer la plupart des opérations.
  • Internet Explorer utilise à la fois cmath et certains blocs de code écrits en assembleur . Ici, même des méthodes trigonométriques de processeurs ont été utilisées - dans le cas où le navigateur était compilé pour des processeurs ayant des instructions similaires.

Pour des raisons historiques, les outils utilisés pour effectuer des calculs dans différents moteurs JS ont changé. Ainsi, V8 a utilisé sa propre solution pour les calculs, puis a utilisé le port JavaScript fdlibm, puis seulement la version C de fdlibm.

▍ Pourquoi est-ce un problème?


Le point ici est que tout cela réduit la capacité de JavaScript à produire des résultats uniformes pour résoudre tous les problèmes impliquant des calculs mathématiques. Cela est particulièrement difficile pour la science des données. J'aimerais que JavaScript soit mieux adapté pour effectuer des calculs de science des données dans un navigateur. De plus, l'incapacité à produire des résultats uniformes entraîne l'aggravation de la crise de reproductibilité , caractéristique de toutes les sciences. Cela ne veut pas mentionner certains autres problèmes JavaScript, tels que les fonctionnalités de saisie de nombres et le manque d'une bibliothèque largement utilisée pour travailler avec des trames de données.

▍3. Utilisation de bibliothèques spécialisées


Il existe un moyen fiable pour nous de faire les calculs en JavaScript. Elle consiste à utiliser des bibliothèques spécialisées. Ainsi, la bibliothèque stdlib implémente des calculs de haut niveau en utilisant uniquement des opérations arithmétiques. Les calculs arithmétiques sont entièrement décrits dans les spécifications, ils sont standard, donc les résultats retournés par stdlib nous donnent, quelle que soit la plateforme sur laquelle le code est exécuté, des résultats complètement uniformes.

Ceci est réalisé au prix de la complexité et de la rapidité des décisions. Les méthodes stdlib ne sont pas aussi rapides que les méthodes intégrées. De plus, afin de "simplement compter le sinus", vous devez connecter une bibliothèque entière au projet.

Mais, si vous pensez plus largement, c'est tout à fait normal. La plate-forme WebAssembly, par exemple, ne donne au programmeur aucun moyen d'effectuer des calculs mathématiques de haut niveau. Dans la documentation correspondante, il est recommandé d'inclure indépendamment les implémentations des mécanismes correspondants dans vos propres modules:

«WebAssembly n'inclut pas ses propres implémentations de fonctions mathématiques - comme sin, cos, exp, pow, etc. La stratégie de WebAssembly pour ces fonctions est de permettre aux développeurs de les implémenter en tant qu'outils de bibliothèque dans la plate-forme WebAssembly elle-même (notez que les instructions sin et cos de la plate-forme x86 sont lentes et inexactes, et de nos jours, en tout cas, essayez de ne pas utiliser). "

C'est ainsi que les langages compilés fonctionnaient toujours: lorsqu'un programme écrit en C est compilé, les méthodes importées de math.hsont incluses dans le programme compilé.

Utilisation de la valeur epsilon


Si quelqu'un ne souhaite pas inclure la bibliothèque stdlib dans son projet JavaScript pour effectuer des calculs, mais doit tester le code qui effectue des calculs complexes, il devrait probablement recourir à la méthode déjà utilisée dans la bibliothèque de statistiques simples. Il s'agit d'utiliser une valeur epsilonqui définit les limites dans lesquelles les différences de nombres ne sont pas prises en compte. Si nous considérons les options d' utilisation du symbole epsilon en mathématiques, nous pouvons dire que j'en parle comme d'une «petite valeur positive arbitraire». Simple-statistics utilise la valeur epsilon d'égale 0.0001.

Si vous avez besoin de savoir si deux nombres sont égaux, une condition du formulaire est vérifiéeMath.abs(result — expected) < epsilon. Si cette condition s'avère être vraie, alors nous pouvons dire que la différence entre les nombres se situe dans la plage donnée et les considérer comme égaux.

Ajouts


▍ Précision


Les commentateurs sur Twitter ont indiqué que les variations des résultats obtenus dans l'exemple sont en dehors de la plage de chiffres significatifs du nombre à virgule flottante. D'un point de vue technique, c'est correct, et cela signifie que vous pouvez trouver un moyen plus précis de comparer les nombres que celui qui utilise la valeur epsilon. Mais en pratique, la même histoire ici - les chiffres à la fin du nombre affectent le résultat et introduisent des inexactitudes dans le résultat final. De plus, les exemples donnés ici ne sont pas exhaustifs. Le fait est que les fonctionnalités d'implémentation des interpréteurs JavaScript peuvent, sans s'écarter de la spécification, conduire à l'apparition de différences dans la plupart des résultats numériques.

▍JavaScript


Je ne veux pas critiquer JavaScript. Je crois que JavaScript a fait un compromis justifiable, compte tenu de l'incertitude de l'avenir et du nombre de plates-formes sur lesquelles les implémentations de langage sont créées. Je dois dire qu'il est très difficile de comparer directement JavaScript et d'autres langages. Le point est l'écosystème JavaScript. Le fait qu'il existe en même temps de nombreux interprètes de la même langue est totalement atypique pour les autres langues. Et cela, en plus, est l'une des principales forces de JavaScript. De plus, on ne peut manquer de dire que ce sont des phénomènes d'un plan complètement différent de ceux qui se produisent dans la langue elle-même. Et JavaScript change au fil du temps et beaucoup de bonnes choses y apparaissent .

TdStdlib ou epsilon?


Je pense qu'en pratique, dans la plupart des cas, il vaut la peine d'utiliser l'approche qui implique l'utilisation de la valeur epsilon. La bibliothèque stdlib est un merveilleux outil puissant, mais le prix d'inclure une bibliothèque supplémentaire pour les calculs mathématiques dans le projet peut être assez élevé. Et dans la plupart des cas, les petites différences dans les résultats des calculs n'ont pas d'importance pour les applications.

Sommaire


Je voudrais ici tirer des conclusions de ce qui précède et partager quelques réflexions.

  1. , , , . . — « ». , , , Math.sin , , , . , , , , , . , , , .
  2. . , , Node.js, , , simply-statistics. , , , , . — .
  3. , . V8, , . , . , , , .

Chers lecteurs! Avez-vous rencontré des problèmes concernant les modifications des résultats de calcul lors de la mise à niveau vers de nouvelles versions de Node.js?


All Articles