如何可靠检测JavaScript变量是否以32位整数格式存储
从asm.js这类库以及Brendan Eich的多次公开演讲中可以总结出一条经验:只要保持一致使用,JavaScript的32位整数运算速度远快于其默认采用的IEEE 754浮点数。
我正在开发一款仅运行在V8引擎上的电子游戏,希望在性能敏感的核心模块对32位整数做基准测试。我已知可以通过位运算符轻松强制数值在引擎内部以32位格式存储,示例如下:
const myValue = 12 | 0;
当然,只有确认数值没有在某段逻辑中被意外转换为浮点数,基准测试的结果才是有效的。我希望编写测试用例验证这一点,请问是否存在可靠方法,能够确认某一变量当前在引擎内部确实是以32位整数格式存储的?
补充说明:
请注意我并非在寻找去除小数部分的方案,Math.floor()和Math.trunc()这类函数已经可以实现该需求(可通过x === Math.floor(x)验证)。我需要的是能够利用引擎底层运行机制的检测方案:浏览器引擎有时会将数值作为浮点数处理,有时又会作为32位整数处理(JavaScript中x = 3并不必然以整数格式存储)。32位强制转换提升性能的典型案例是~~number写法,基准测试显示其速度远快于Math.floor(number),但该写法仅对小于2,147,483,647的数值有效,超过该阈值会触发整数溢出。
V8没有在标准JavaScript API中暴露直接查询数值内部存储格式的方法,但针对V8专属的开发场景,有三类可落地的验证方案,按可靠度从高到低排列:
测试环境原生语法检测
本地跑基准测试时,可以给V8(或Electron)加上--allow-natives-syntax启动参数,开启引擎内置的原生调试函数,直接判断值的存储类型:// 64位V8环境下,Smi(标签小整数)正好对应32位有符号整数范围,即你需要的int32存储格式 function isStoredAsInt32(val) { return typeof val === 'number' && %IsSmi(val); }注意这个方法只能在本地测试环境使用,生产环境下V8会默认禁用所有
%开头的原生内置函数,直接调用会抛语法错误。基于编译器行为的黑盒验证
如果不想改启动参数,可以通过V8优化编译器的行为间接判断:V8的TurboFan编译器会为稳定的int32类型值生成专用的整数运算指令,一旦值被转为浮点数、流入非整数运算路径,就会触发去优化(Deopt)。
测试时给V8加上--trace-deopt启动参数,跑完核心模块的逻辑后查看日志,如果没有出现和数值类型变更相关的去优化记录,就说明这段逻辑里的数值全程保持了int32存储格式,没有意外转为浮点数。前置静态约束(生产环境最可靠)
运行时检测始终是滞后的,最稳妥的方案是从代码层面卡死类型边界,避免浮点数流入核心运算路径:- 所有进入核心模块的数值,都必须通过
|0显式转为int32 - 核心模块内所有可能产生非整数结果的运算(比如除法
/、乘法溢出、取模等),运算结果必须追加|0强制转回int32 - 如果使用TypeScript,可以给32位整数加品牌类型做编译期校验,从根源上杜绝浮点数误传入:
type Int32 = number & { __brand: 'int32' }; const int32 = (n: number): Int32 => (n | 0) as Int32; // 核心运算函数只接受Int32类型,传入普通number会直接报类型错误 function coreCalc(a: Int32, b: Int32): Int32 { return int32(a + b); }- 所有进入核心模块的数值,都必须通过
最后需要注意一个常见误区:不是只要给值做过一次|0就会永远保持int32格式。只要后续对这个值做了任何返回浮点数的操作(比如未加|0的除法、传入接受任意number类型的工具函数、参与返回非整数的数学运算),V8就会自动把它转为堆上存储的浮点数格式,之前的整数类型标记也会失效。
内容的提问来源于stack exchange,提问作者aggregate1166877

