这篇文章来自我们团队的 李云蛟 同学,他非常擅长BFF层的开发,同时也是位德艺双馨的单口相声爱好者,和他一起工作生活仿佛令人置身于《我爱我家》、《编辑部的故事》的拍摄现场。今天让我们一起看看除了夜阑卧听京韵大鼓、大茶缸子泡茉莉花茶外,他还喜欢关注什么
背景
近日在twitter看到这样一则推文,有一个"耸人听闻"的小标题:
如此吸人眼球,让人不得不跟着一探究竟
推文介绍
推文分为以下几个内容:
1. 先从数据的存储入手
2. 再从计算本身阐述
3. 推导原理
4. 被人质疑实现对照组后更正错误
结论
使用0-x 比单纯的使用 -x 效率高20%-50%
一些知识点
这则推文的第一张图片用于解释提出理论的前置条件和理论基础,可这么一张图片传递的信息量有限。涉及到以下知识点,需要我们去了解
1. 数字的内存分配
看过mdn 中 number 一节我们知道,js中的数字只有一种编码形式,那就是双精度64位 IEEE 754编码规范,所以js这种弱类型语言对于数字(bigint出现之前)的定义就很简单且随意, 但是推文中说的又是怎么回事呢?
原来在js引擎的实现中,考虑到日常计算业务出现最多的其实是一些有符号的小整数,但是使用双精度64位去存储有符号小整数效率削微低,于是上有政策下有对策,厂商们在实现js引擎时会将有符号小整数单独存储到一块存取方便的内存中。
比如V8引擎,在遇到数字时会开辟一块32位内存进行存储以提高效率。其中最后一位作为标记位,用于指示该数字是Smi(有符号小整数)还是一个双精度64位。
如果是Smi,剩下31位用来保存这个数字的值。如果是双精度64位,数值被存储在一个名叫HeapNumber的实例中,剩余31位存储指向这个HeapNumber实例的指针。
V8中,Smi的取值效率高于HeapNumber; 更多内容请看
2. 0与负0
通过上面第一步的阐述我们了解, js引擎在处理数字存储的时候并没有按照ECMAScript规范规定的那样只单纯的用双精度64位去存储,为了效率还是将有符号小整数区别对待,存到一个31位的便于存取内存中。
这就导致给js的数字表示带来一个难题。因为有符号小整数表示的数据范围是 2^31-1 至 -2^31, 那么众所周知这种整数表示法-0的二进制编码被用于表示-2^31, 导致Smi存储的这些有符号小整数里根本就没有-0, 可ECMAScript规范规定数字只能用 双精度64位表示, -0 必须是被程序员可见的。
为了解决这一问题,-0 被V8 以 双精度64位的方式保存在HeapNumber 中;
我的结论
通过上述分析我们知道, -0独特的保存方式让它在参与有符号小整数运算时兜了个小圈子, 也就是推文中a=0-x 和 a=-x 效率问题。 但是这种提法是有瑕疵的, 真正引发效率变化的只有 -0 和超出js引擎单独处理的有符号小整数范围外的数字, 其他有符号小整数的正负值因为都在31位里,0-x 与 -x 效率并无差别。
我们也做如下的实验
1. 0 与 -0 对照
const dataInt = [0-0,1];
const dataMix = [-0,1];
const length = 100;
const idxInt = [];
const idxMix = [];
for(let i=0; i<length; i++) {
idxInt.push(i%2);
idxMix.push(i%2);
}
对比组
let sum = 0;
for(let i=0; i<length; i++) {
sum += dataInt[idxInt[i]];
}
实验组
let sum = 0;
for(let i=0; i<length; i++) {
sum += dataMix[idxMix[i]];
}
可以看到实验组比对比组的效率低了 31%
2. 双-1对照
我们修改代码,将实验改为 0-1 和 -1 的存取对比,其余不变 实验地址
const dataInt = [0,0-1];
const dataMix = [0,-1];
const length = 100;
const idxInt = [];
const idxMix = [];
for(let i=0; i<length; i++) {
idxInt.push(i%2);
idxMix.push(i%2);
}
二者并没有太大差异,多次对比后 二者互有高低,性能差别忽略不计