从asm.js等库和Brendan Eich的许多演讲中吸取的教训是,JavaScript的32位整数比其默认的IEEE 754浮点数快很多,前提是它们被一致地使用。
我正在编写一款专门使用V8的视频游戏,希望在关键领域对32位int进行基准测试,并了解到我们可以通过使用逐位运算符在内部轻松地强制值变为32位,例如:
const myValue = 12 | 0;
当然,只有当我知道某个东西没有意外地将它变成某个浮点值时,基准才有效。我想写一些测试来验证这一点。是否可以可靠地确认变量当前实际上存储为32位整数?
编辑:
请注意,我不是在寻找删除小数点的方法。像Math.floor()
和Math.trunc()
这样的函数已经可以做到这一点(可通过x === Math.floor(x)
进行测试)。我所寻找的是专门利用不明显的复杂性,例如浏览器引擎有时会将数字视为浮点,但有时会将一个数字视为32位整数(x = 3
在JavaScript中不一定是整数)。32位强制加速的一个真实例子是使用像~~number
这样的代码,它比Math.floor(number)
快得多,但只适用于小于2147483647的数字(因为除此之外,你会溢出)。
V8开发人员。
是否可以可靠地确认变量当前实际上存储为32位整数?
不是来自JavaScript。
也没有特殊的发动机内省功能;其中一个原因是,这种情况会根据具体情况而改变。例如,与稍后启动的优化编译器相比,基线层(解释器或简单编译器)可能会对如何存储内容做出不同的决定。
说到后者,您可以做的一件事是使用--print-opt-code
(在启用了反汇编程序支持的构建中)检查V8中生成的优化代码。不过这相当乏味。
之间还有一个关键的区别
- 函数内部后续操作之间的中间计算结果的极短期存储。V8的优化编译器经常使用32位表示;这完全是自动发生的,而不需要你做任何特别的事情
- 最终计算的长期存储会导致函数返回值、对象属性、数组、全局/外部范围变量等。V8从不使用32位整数表示
(注意1:Int32Array
s是一个必要的例外,但在你过于兴奋之前,请记住TypedArray既有优点也有缺点;特别是它们更优化,可以拥有几个大的长寿命阵列,而普通的旧Array
s总体上更快,可以拥有许多小的长寿命数组。)
(注2:这可能会也可能不会随着时间的推移而改变。)
(注意3:V8尽可能使用31位带标记的整数。就像32位表示的短寿命中间体一样,您无法直接观察到这一点。)
从asm.js等库和Brendan Eich的许多演讲中吸取的教训是,JavaScript的32位整数比其默认的IEEE 754浮点数快得多,前提是它们的使用一致。
我认为那句话经不起推敲。避免int<->;float转换当然有助于提高性能,但在许多情况下,使用float并不比使用int慢
(其他挑剔之处:asm.js不是一个库,Brendan也不是JavaScript性能的权威,"JavaScript的32位整数"一词误导性地暗示JavaScript有32位整数,你可以选择使用,但它没有。)
我们可以通过使用逐位运算符在内部轻松地强制值变为32位,例如:
const myValue = 12 | 0;
不完全是。就JavaScript语义而言,12
和12 | 0
之间没有区别。就实现细节而言,在V8中,const v = 12 | 0;
被解析器固定折叠,因此与const v = 12;
(或const v = 12.0;
)完全等效。
您可能想在这里引用的asm.js样式模式是在操作之后放置| 0
注释,如let x = (y+z)|0
中所示。这确实有影响:它改变了可观察的语义,和大多数现代引擎的优化编译器都会将其理解为一个提示,即将值保存在整数寄存器中可能是最方便/最有效的。这仍然不是强制整数表示的方法,但这是提示,整数表示可能是一个不错的选择。
类似
~~number
的代码,比Math.floor(number)
快得多
这是一个完全误导性的微基准测试的典型例子。一个明显的迹象是:当你每秒看到超过10亿次操作时,可以肯定的是,优化编译器已经完全优化了你的测试,而你根本没有测量到任何东西。此外,measurethat.net的框架在过去被证明会产生虚假的结果;我现在不想花一个小时来弄清楚这次发生了什么奇怪的事情,除了死代码消除之外,我的猜测是,慢速情况下会花太多时间分配堆号,以至于floor
操作几乎不会出现在配置文件中在从任何微基准中得出任何有用结论之前,您必须了解它在幕后的实际作用分析它是最低限度的。检查生成的代码更好。各种JS基准测试站点都完全无法满足这一基本要求。
作为一个快速反例,以下两个函数编译为完全相同的优化代码:
function f() { for (let i = 0; i < 1000; i++) i = ~~i; }
function g() { for (let i = 0; i < 1000; i++) i = Math.floor(i); }
如果将循环体更改为~~(i + 0.1)
和Math.floor(i + 0.1)
,那么这两个版本都将执行float和back的转换,因此该示例不能作为支持任何"循环体"的参数;整数更快";宣称在这种情况下,将有20%的差异有利于~~
,但(a)这并不完全是";疯狂";,以及(b)这是由于Math.floor
必须执行更多的机器指令来正确处理-0
和NaN
这样的情况,~~
可以将其截断为0
。
关于评论中所说的话:
(3 | 0) === (3.1 - 0.1) // true
这只能证明在JavaScript中,所有数字的行为都像是双精度的。它不会告诉您任何关于引擎如何在内部存储数字的信息。
数字类型(javascript中存在的唯一类型的数字)
嗯,现在也有BigInts。但是,由于BigInts必须担心可能会使变大(也因为他们还没有得到那么多的优化关注),所以对于小值来说,他们并不比Numbers快。
您必须查看优化器生成的字节码
差不多。字节码未优化。优化编译器生成机器代码,而不是字节码。
您说过
3.0
是一个32位int,(根据规范)这是完全错误的(它是一个64位IEEE 754浮点)
这很棘手。根据JS规范,所有数字都是64位浮点。这个问题是关于引擎选择的32位存储,而3.0
绝对可以由引擎存储为32位整数。V8的解析器不区分3.0
和3
,V8最初通常将其存储为31位(原文如此!)整数。但你不可能知道——一定没有;因为根据JS规范,所有数字的行为都必须像用64位浮点表示一样。
这实际上是一款已经完成的独立视频游戏[…]我想,如果可能的话,为什么不挤出2%的额外性能呢?这是(可能为时过早)优化,我肯定可以不用,但我认为实验不会伤害
这些信息本来属于最初的问题,因为它为什么是有用的答案或不是有用的答案提供了相关的框架
用不会移动针头的东西做实验是浪费时间;是否符合";伤害;符合您的定义
如上所述,您无法检查内部表示选项,但可以提供提示。如果你有大量的计算可以保留在整数域中,你可以做的一个实验是在这些运算之后添加| 0
,看看这是否有影响
如果我站在你的立场上,我会首先使用评测来查看有问题的函数在全局中是否重要。