Acerca de los métodos mutables de un objeto matemático en JavaScript

Hoy publicamos una traducción de un artículo sobre cálculos matemáticos en JavaScript, que es una versión escrita de la presentación de su autor en WaffleJS . Y esta declaración en sí misma fue una continuación de esta conversación en Twitter.


Educación matemática

Interés en las matemáticas


Todo comenzó con mi educación matemática, con el recibo que retrasé un poco. Cuando estudié ciencias de la computación, pude comunicarme con maestros de matemáticas de clase mundial, pero perdí esta oportunidad. No me gustaban las matemáticas: los temas estudiados estaban muy lejos de la práctica. Además, ya estaba fuera de balance por un programa profundamente teórico de capacitación en informática. Entonces creí que estaba divorciada de la vida y, en su mayor parte, sigo pensando que sí.

Pero ahora, unos años después de completar mis estudios, una sed de aprender matemáticas despertó en mí. Me inspiró el efecto que incluso una pequeña aplicación de conocimiento matemático puede tener en mi trabajo y en mi pasatiempo. Pero no tenía un plan de estudio claro.

Luego, en 2012, encontré una manera de estudiar matemáticas comenzando a trabajar en el proyecto de Estadísticas simples . Desde entonces he expandido y apoyado este proyecto. Ahora incluye implementaciones de muchos algoritmos, resultó ser una de las bibliotecas de JavaScript matemáticas más " estrelladas ". Y, aparentemente, la gente realmente usa esta biblioteca.

Pero empecé a trabajar en 2012. Si hablamos sobre cómo la tecnología ha cambiado durante este tiempo, entonces fue hace mucho, mucho tiempo. Desde entonces, 8 lanzamientos LTS de Node.js han sido puestos en libertad . Desde entonces, JavaSript y los entornos en los que funcionan los programas escritos en este lenguaje han cambiado mucho. En 2012, la biblioteca React aún no existía, entonces el primer compromiso con el proyecto Babel aún no se había realizado.


El paso del tiempo

A lo largo de los años, noté que mis pruebas fallaban al actualizar Node.js. Por ejemplo, podría tener algo como esta prueba:

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

Esta prueba funciona bien en Node.js v10, pero se rompe en Node.js v12. Y aquí no se prueba un método súper complicado: la función se gammaimplementa utilizando funciones estándar de JavaScript Math.pow, Math.sqrty Math.sin.

Aritmética


Sé lo que puedes pensar aquí: aritmética. En Twitter, las acaloradas discusiones estallan periódicamente debido a los resultados del cálculo de la siguiente expresión:

0.1 + 0.2 = 0.30000000000000004

Pero, como ya escribí , todos los lenguajes de programación populares se comportan de esta manera, incluso los antiguos y pedantes como Haskell. La aritmética de punto flotante puede parecer extraña, pero se comportan igual, su comportamiento está bien documentado. Es decir, estamos hablando del estándar IEEE 754 , cuyos requisitos se implementan estrictamente en lenguajes de programación. Entonces, el problema no está en la aritmética: la implementación de la suma, la resta, la división y la multiplicación en lenguajes, la programación se puede decir que está "tallada en piedra".

Objeto matemático


Mi problema estaba en el objeto estándar de JavaScript Math. En particular, en todos los métodos de este objeto.

Técnicas tales como Math.sin, Math.cos, Math.exp, Math.pow, Math.tan- es los ingredientes básicos para geométricas cálculos y otros. Cuando entendí esto, comencé a estudiar por separado los cambios en el comportamiento de los métodos básicos del objeto Mathen diferentes versiones de Node.js. Aquí hay unos ejemplos.

Calculo Math.tanh(0.1):

// Node 4
0.09966799462495590234
// Node 6
0.09966799462495581907

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

// Node 10
0.03703703703703703498
// Node 12
0.03703703703703702804

Peor aún, este problema no solo es aparente en Node.js. Lo mismo sucede en los navegadores y en otros entornos que admiten JavaScript.

Esto nos lleva a la siguiente pregunta: ¿qué es el cálculo matemático?


Representación gráfica de los cálculos Los

métodos trigonométricos son fáciles de visualizar. Si tienes un solo círculo y varios meses de secundaria a tu disposición, entonces sabes que el coseno y el seno son las coordenadas de cierto punto en el borde del círculo, y que las gráficas de las funciones pecado y cos parecen ondas. De hecho, en la escuela secundaria están estudiando cómo obtener estos valores, pero el método utilizado para esto, la serie Taylor , se basa en una serie infinita, y no es fácil para una computadora resolver tales problemas.

Esto es lo que puedes aprender de Wikipediacon respecto al algoritmo de cálculo de seno: “No existe un algoritmo estándar para calcular el seno. IEEE 754-2008, el estándar más utilizado para cálculos de coma flotante, no afecta el cálculo de funciones trigonométricas como un seno ".

Las computadoras usan muchas aproximaciones y algoritmos diferentes para realizar cálculos, algo así como CORDIC , todo tipo de trucos difíciles y tablas de búsqueda. Toda esta heterogeneidad explica la presencia de muchas bibliotecas de matemáticas rápidas en GitHub . El hecho es que hay muchas formas de implementar el método Math.sin. Y otras funciones también. Por ejemplo, como sabes, Quake III Arena usó un equipo más rápidoreemplazando el método estándar de cálculo de la raíz cuadrada inversa para acelerar el renderizado.

Como resultado, los cálculos matemáticos son el resultado de la implementación de ciertos algoritmos. En la práctica, se utilizan muchos algoritmos comunes y sus variantes.

La especificación de JavaScript, en lugar de especificar qué algoritmo particular usar en implementaciones de lenguaje, les da mucho margen de maniobra a las implementaciones en términos de funciones utilizadas en los cálculos matemáticos.

Esto es lo que dice el estándar sobre esto (ECMA-262, edición 10, sección 20.2.2):

"El comportamiento de las funciones acos, acosh, asin, asinh, atan, atanh, atan2, cbrt, cos, cosh, exp, expm1, hypot, log, log1p, log2, log10, pow, random, sin, sinh, sqrt, tan y tanh no se describe completamente aquí, con la excepción de los requisitos para la devolución de ciertos resultados para valores específicos de los argumentos, que son casos límite notables ".

No sé cómo funcionan las actividades internas de los miembros del comité responsable del estándar ECMA-262, pero creo que hicieron el estándar para que no hubiera una crisis de compatibilidad en JavaScript si Intel o AMD emitieran nuevas instrucciones matemáticas ultrarrápidas en sus procesadores frescos.

Debido al hecho de que hay muchos intérpretes de JavaScript ampliamente utilizados, debido al hecho de que JavaScript se usa a menudo en los navegadores, y entre los navegadores todavía hay algo parecido a la competencia, y al hecho de que incluso las implementaciones populares de JavaScript están bajo presión constante y obligados a evolucionar rápidamente, proporcionando el mejor rendimiento ... debido a todo esto, tenemos lo que tenemos. Cualquiera que use JavaScript encontrará regularmente el hecho de que en diferentes implementaciones los resultados de los cálculos matemáticos realizados por medio del objeto Mathson diferentes.

Esto no es tan significativo en otros lenguajes interpretados, ya que generalmente tienen algunas implementaciones "canónicas". Por ejemplo, esto es cierto para el intérprete de Python.

¿Dónde se realizan los cálculos?


Ahora echemos un vistazo más de cerca exactamente donde se realizan los cálculos. En el caso de JavaScript, hay tres áreas en las que se realizan cálculos matemáticos básicos:

  1. UPC.
  2. Intérprete de lenguaje (código C ++ y C de una implementación específica de JavaScript).
  3. Código JavaScript, por ejemplo, código para bibliotecas especializadas.

▍1. UPC


La primera idea que se me ocurrió cuando pensé en los lugares donde se realizan los cálculos fue que los cálculos se realizan en el procesador. Sugerí que, dado que los procesadores implementan cálculos aritméticos, también pueden implementar algunos cálculos más complejos. Resultó que los procesadores tienen instrucciones para realizar cálculos trigonométricos y de otro tipo, pero estas instrucciones rara vez se usan. Por ejemplo, la implementación del cálculo de seno en procesadores con arquitectura x86 no fue muy popular, ya que esta implementación no necesariamente resulta ser más rápida que las implementaciones de software (aquellas que usan operaciones aritméticas de procesador). Además, no es necesariamente más preciso que las implementaciones de software.

Intel también sufrió una vergüenza debido a la muy fuerteSobreestimación de la precisión de las operaciones trigonométricas en la documentación. Tales errores son especialmente trágicos debido al hecho de que un microchip, a diferencia de un programa, no puede ser parcheado.

▍2. Intérprete de idiomas


Así es como funcionan los subsistemas informáticos en la mayoría de las implementaciones de JavaScript. Implementan estos subsistemas de varias maneras.

  • Los motores V8 y SpiderMonkey usan puertos de biblioteca fdlibm para los cálculos , que son ligeramente diferentes. Esta biblioteca, originalmente escrita en Sun Microsystems, se ha transmitido de generación en generación.
  • JavaScriptCore (Safari) usa la biblioteca cmath para realizar la mayoría de las operaciones.
  • Internet Explorer usa cmath y algunos bloques de código escritos en ensamblador . Aquí incluso se utilizaron métodos trigonométricos de procesadores, en el caso de que el navegador se compilara para procesadores que tenían instrucciones similares.

Por razones históricas, las herramientas utilizadas para realizar cálculos en diferentes motores JS han cambiado. Entonces, V8 usó su propia solución para los cálculos, luego usó el puerto JavaScript fdlibm, y solo entonces la versión C de fdlibm.

▍ ¿Por qué es esto un problema?


El punto aquí es que todo esto reduce la capacidad de JavaScript para producir resultados uniformes en la resolución de cualquier problema que implique cálculos matemáticos. Esto es especialmente difícil en Data Science. Me gustaría que JavaScript sea más adecuado para hacer cálculos de Data Science en un navegador. Además, la incapacidad de producir resultados uniformes significa el agravamiento de la crisis de reproducibilidad , que es característica de todas las ciencias. Esto sin mencionar otros problemas de JavaScript, como las características de escribir números y la falta de una biblioteca ampliamente utilizada para trabajar con marcos de datos.

▍3. Usando bibliotecas especializadas


Hay una forma confiable de hacer los cálculos en JavaScript. Consiste en utilizar bibliotecas especializadas. Entonces, la biblioteca stdlib implementa cálculos de alto nivel utilizando solo operaciones aritméticas. Los cálculos aritméticos se describen completamente en las especificaciones, son estándar, por lo que los resultados devueltos por stdlib nos dan, independientemente de la plataforma en la que se ejecuta el código, resultados completamente uniformes.

Esto se logra a costa de la complejidad y la velocidad de las decisiones. Los métodos stdlib no son tan rápidos como los métodos integrados. Además, para "solo contar el seno", debe conectar una biblioteca completa al proyecto.

Pero, si piensas más ampliamente, esto es completamente normal. La plataforma WebAssembly, por ejemplo, no le da al programador ningún medio para realizar cálculos matemáticos de alto nivel. En la documentación para ello, se recomienda que incluya independientemente implementaciones de los mecanismos correspondientes en sus propios módulos:

“WebAssembly no incluye sus propias implementaciones de funciones matemáticas, como sin, cos, exp, pow, etc. La estrategia de WebAssembly para tales funciones es permitir que los desarrolladores las implementen como herramientas de biblioteca en la plataforma WebAssembly (tenga en cuenta que las instrucciones sin y cos de la plataforma x86 son lentas e inexactas, y en estos días, de todos modos, intenta no usar) ".

Así es como siempre han funcionado los lenguajes compilados: al compilar un programa escrito en C, los métodos importados math.hse incluyen en el programa compilado.

Usando el valor epsilon


Si alguien no desea incluir la biblioteca stdlib en su proyecto de JavaScript para realizar cálculos, pero necesita probar el código que realiza algunos cálculos complejos, entonces probablemente debería recurrir al método que ya se utiliza en la biblioteca de estadísticas simples. Se trata de utilizar un valor epsilonque defina los límites dentro de los cuales no se tienen en cuenta las diferencias en los números. Si consideramos las opciones para usar el símbolo épsilon en matemáticas, entonces podemos decir que estoy hablando de él como un "pequeño valor positivo arbitrario". Simple-statistics utiliza el valor épsilon de igual 0.0001.

Si necesita averiguar si dos números son iguales, se verifica una condición del formularioMath.abs(result — expected) < epsilon. Si esta condición resulta ser cierta, entonces podemos decir que la diferencia entre los números cae dentro del rango dado y considerarlos iguales.

Adiciones


▍ precisión


Los comentaristas en Twitter indicaron que las variaciones en los resultados obtenidos en el ejemplo están fuera del rango de dígitos significativos del número de coma flotante. Desde un punto de vista técnico, esto es correcto, y esto significa que puede encontrar una forma más precisa de comparar números que la que usa el valor epsilon. Pero en la práctica, la misma historia aquí: los números al final del número afectan el resultado e introducen imprecisiones en el resultado final. Además, los ejemplos dados aquí no son exhaustivos. El hecho es que las características de implementación de los intérpretes de JavaScript pueden, sin apartarse de la especificación, dar lugar a la aparición de diferencias en la mayoría de los resultados numéricos.

▍JavaScript


No quiero criticar JavaScript. Creo que JavaScript hizo un compromiso justificable, teniendo en cuenta la incertidumbre del futuro y la cantidad de plataformas en las que se crean las implementaciones de lenguaje. Debo decir que es muy difícil comparar directamente JavaScript y otros idiomas. El punto es el ecosistema de JavaScript. El hecho de que al mismo tiempo haya muchos intérpretes del mismo idioma es completamente atípico para otros idiomas. Y esto, además, es una de las principales fortalezas de JavaScript. Además, uno no puede dejar de decir que estos son fenómenos de un plan completamente diferente a los que ocurren en el lenguaje mismo. Y JavaScript cambia con el tiempo y aparecen muchas cosas buenas .

TdStdlib o epsilon?


Creo que en la práctica, en la mayoría de los casos, vale la pena usar el enfoque que implica el uso del valor épsilon. La biblioteca stdlib es una herramienta poderosa y maravillosa, pero el precio de incluir una biblioteca adicional para cálculos matemáticos en el proyecto puede ser bastante alto. Y en la mayoría de los casos, las pequeñas discrepancias en los resultados de los cálculos no son importantes para las aplicaciones.

Resumen


Aquí me gustaría sacar conclusiones de lo anterior y compartir algunas ideas.

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

¡Queridos lectores! ¿Ha encontrado problemas con respecto a los cambios en los resultados de los cálculos al actualizar a nuevas versiones de Node.js?


All Articles