这是我2月在WaffleJS上发表的演讲的书面版本,它本身是10月以来Twitter对话的扩展。
好的,所以从我延迟的数学教育开始。作为我的计算机科学计划的一部分,我可以与世界一流的数学教授接触,而我最浪费的是接触。我不喜欢数学:将话题从实践中删除,并且我已经对理论性过高感到沮丧,而且-我当时以为并且大部分时间仍然如此-脱节的CS程序。
不幸的是,毕业几年后,我对数学产生了渴望。看到我如何才能将一点点数学知识应用到我的工作中爱好使我受到启发。但是我没有明确的学习方法。
因此,我从2012年开始从事简单统计的学习,从那时起,我就扩展并维护了该项目。现在,它包含许多不同的算法,是最受好评的JavaScript数学项目之一,并且可能被人们使用。
但是我是从2012年开始的。在很久以前的技术时代。从那时到现在,已经有8个LTS版本的Node。 JavaScript及其环境已经发生了根本性的变化。 2012年是在引入React或Babel的第一次承诺之前。
因此,我多年来注意到的是,当我更新Node时,测试一直在中断。我会进行类似的测试:
那将在Node v10中工作,而在Node v12中中断。这不是什么复杂的方法:gamma是通过算术,Math.pow,Math.sqrt和Math.sin实现的。
所以我知道您可能在想什么:算术。 Twitter上的JavaScript为此行为引起了很多关注:
正如我在剖析的JavaScript故事中所写的那样,这是每种流行编程语言的行为,甚至是像Haskell这样笨拙的学究语言。浮点算法可能很奇怪,但是它非常一致且规格明确:IEEE 754规范得到严格实施。因此,这不是算术运算:加法,减法,除法和乘法是一成不变的。
那是数学。特别是Math之后的所有方法。
诸如Math.sin,Math.cos,Math.exp,Math.pow,Math.tan之类的方法:几何和基本计算的基本要素。我开始隔离版本之间基本功能行为的更改。例如:
更糟糕的是,不仅仅是Node的行为正在改变:浏览器和其他使用JavaScript的地方也在改变。
三角法很容易显示:给定一个单位圆和几个月的高中学习,您知道余弦和正弦将使您在边缘上保持坐标,并且如果在X& amp;上绘制,它们将很少弯曲。是的。实际上,这些方法是您在高级课程中会学到的,但是您使用的方法-泰勒级数-依赖于无限级数,这对于计算机求解来说非常费力。
“没有用于计算正弦的标准算法。 IEEE 754-2008是浮点计算使用最广泛的标准,没有解决计算正弦等三角函数的问题。”
计算机使用各种不同的估计和算法进行数学运算,例如CORDIC和各种作弊算法和查找表。这个异类解释了您可以在GitHub上找到的所有“ fastmath”库:实现Math.sin的方法不止一种。著名的Quake III Arena使用更快的替代平方根倒数方法来加快渲染速度。
因此数学是作为算法实现的,实践中有多种常见算法-以及这些算法的变体。
JavaScript规范没有告诉实现选择算法,而是在如何实现这些基本功能方面给了他们很大的回旋余地。
函数acos,acosh,asin,asinh,atan,atanh,atan2,cbrt,cos,cosh,exp,expm1,hypot,log,log1p,log2,log10,pow,pow,random,sin,sinh,sqrt,tan的行为和tanh在此处未明确指定,只是要求某些表示感兴趣边界情况的参数值需要特定结果。
我不知道标准委员会的内部运作方式,但我想他们想确保万一Intel或AMD在新处理器中引入超快速新数学指令时,JavaScript不会出现兼容性危机。
因为有很多常用的JavaScript解释器,所以JavaScript通常是通过Web浏览器使用的,并且Web浏览器之间仍然存在一些竞争,并且因为甚至流行的JavaScript实现也面临着迅速发展成为性能最高的压力...最重要的是,这很重要。实际上,您会定期遇到数学上的差异。
在其他解释语言中,这无关紧要,因为它们倾向于具有“规范”解释器:大多数情况下,您使用Python语言的Python解释器。
接下来,让我们放大这些数学实现的实现位置。在JavaScript中,可以在三个地方发生基本数学运算:
这是我的第一个猜测:我假设由于CPU执行算术运算,因此它们可能会执行一些高级数学运算。事实证明,CPU确实具有执行三角函数和其他运算的指令,但很少被调用。正弦的CPU(x86)实施并没有引起人们的广泛关注,因为它不可靠地快于软件中的实施(使用CPU上的算术运算),也不准确。
英特尔还因将三角运算的准确性高估了许多程度而受到指责。这种错误特别悲惨,因为与软件不同,您无法修补芯片。
这是大多数实现的方式,并且它们以多种方式实现数学。
V8和对于大多数操作,SpiderMonkey使用fdlibm库的端口(略有不同)。它经过了几代人的传承,最初是由Sun Microsystems编写的。
Internet Explorer使用了一些cmath,但是也使用了一些汇编指令,并且在为具有它们的CPU进行编译时,实际上确实使用了CPU提供的trig方法。
从历史上看,所有这些实现都发生了变化:V8曾经使用本地开发的数学解决方案,然后使用fdlibm到JavaScript的移植,最后才决定使用C的fdlibm。
这就是问题所在的原因:它使JavaScript不能为包括数学在内的任何问题提供一致的结果,而使这种能力无法实现。尤其是数据科学。我希望JavaScript成为浏览器中数据科学的竞争者,并且-在其他一些问题中(例如数字类型和普遍使用的数据框架库的混乱缺乏)-无法产生可复制的结果意味着给应用程序带来更多危机科学中的复制危机。
今天我们可以使用一种出路。 stdlib是一个JavaScript库,仅使用算术即可重新实现更高级的数学。算术是完全指定的和标准的,因此stdlib为您提供的结果在所有平台上也完全一致。
这是以复杂性和速度为代价的:stdlib的速度不如内置方法那么快,并且您需要“仅”一个库来计算正弦。
但是从更广泛的角度来看,这很正常!例如,WebAssembly根本不提供高级数学方法,建议您在模块本身中包含数学实现:
“ WebAssembly不包含自己的数学函数,例如sin,cos,exp,pow等。 WebAssembly针对此类功能的策略是允许将它们作为WebAssembly本身的库例程来实现(请注意,x86的sin和cos指令缓慢且不精确,如今无论如何通常都避免使用它们。)
这就是编译语言一直有效的方式:当编译C程序时,从math.h导入的方法将包含在已编译的二进制文件中。
如果您不想包括stdlib来进行数学运算,但又想测试大量数学代码,则可能必须要做简单统计现在所要做的:使用epsilon。在数学中使用epsilon的5种以上方法中,我指的是“任意小的正数”。这是一个很小的数字。这是简单统计信息的实现:数字0.0001。
然后,您比较Math.abs(结果-预期)< epsilon,以确保您在期望值的范围内,并有一点摆动空间。
在这里,我本人的时间有些紧缺,还有一定的扩展空间。
首先,幕后的东西很少是您所期望的。我们当前的技术堆栈已进行了高度优化,许多优化实际上只是肮脏的把戏。例如,解决Math.sin所需的硬件指令数量因输入而异,因为有很多特殊情况。当您遇到更复杂的情况时,例如“对数组进行排序”,解释器通常会选择多种算法,以便为您提供最终结果。基本上,使用解释语言进行的任何操作的成本都是可变的。
其次,不要太信任系统。我在Node版本之间看到的确实应该是测试库中的错误,代码中的错误或简单统计信息本身。但是在这种情况下,更深入的研究表明,我所看到的正是您所期望的:语言本身的故障。
第三,每个人都在努力。通过阅读V8实现,您可以深刻理解实现解释器所涉及的天才,但是也可以理解,这只是人类在执行实现:他们犯错,并且,正如不断变化的数学算法所证明的那样,总是有空间改善。
精度:Twitter上的评论员指出,示例结果的变化超出了浮点数的有效位数。从技术上讲,这是正确的,这意味着您可能会比使用epsilon提出更精确的比较方法。但实际上是同一回事–尾随数字会传播到结果中并造成实际差异。此外,我给出的示例并不详尽:JavaScript解释器可以在不欺骗规范的情况下,在结果的重要部分引入数值差异。
JavaScript:这不是对JavaScript的批评。我认为,面对不确定的未来和众多平台,该语言已做出适当妥协。而且,很难将其他任何语言直接与JavaScript进行比较,因为JavaScript生态系统-许多共存的语言的不同解释器-相当罕见,它也是JavaScript的最大优势之一。另外,需要明确的是,这与JavaScript语言更改完全不同:这也是正在发生的事情,我对语言的更改感到非常兴奋。
Stdlib或epsilon:大多数情况下的实际解决方案是使用epsilon。 Stdlib引人入胜且功能强大,但是包括一个额外的数学库的成本非常高-在许多情况下,输出中的这些细微差别对应用程序无关紧要。