我翻了ES规范,发现用Typescript写的代码性能更强
不就是个加法
你有一个求和函数:
function sum(a, b) {
return a + b
}
sum(1, 2) // 3
某天你又有了一个字符串拼接的需求,你又写了这么一个函数:
function concatenate(a, b) {
return a + b
}
concatenate('1', '2') // '12'
此时你发现,sum
和concatenate
这两个函数除了名字之外,一模一样,于是你灵机一动,改造成了这样:
function additionOperator(a, b) {
return a + b
}
additionOperator(1, 2) // 3
additionOperator('1', '2') // '12'
看着不错,代码好像更简洁了。
不过...你没意识到,这段代码会产生性能问题,我们来测试下。
创建大量数据:
const numArr = new Array(1000000)
.fill(1)
.map((item, index) => index * Math.random())
const strArr = new Array(1000000)
.fill(1)
.map((item, index) => index + Math.random() + '')
numArr
是一个由数字组成的大数组,strArr
则是由字符串组成。
分别测试一下我们的两个版本,由于数据量比较大,所以每个版本跑五次就可以:
// 版本一
console.time('sum and concatenate')
for (let i = 0; i < 999999; i++) {
sum(numArr[i], numArr[i + 1])
concatenate(strArr[i], strArr[i + 1])
}
console.timeEnd('sum and concatenate')
// 版本二
console.time('additionOperator')
for (let i = 0; i < 999999; i++) {
additionOperator(numArr[i], numArr[i + 1])
additionOperator(strArr[i], strArr[i + 1])
}
console.timeEnd('additionOperator')
从逻辑上来看,以上两个版本完全相同。在我的本机环境运行(node v18.13.0),结果如下:
版本一
sum and concatenate: 6.957ms
sum and concatenate: 5.185ms
sum and concatenate: 6.082ms
sum and concatenate: 4.865ms
sum and concatenate: 5.017ms
版本二
additionOperator: 16.446ms
additionOperator: 13.9ms
additionOperator: 14.2ms
additionOperator: 14.505ms
additionOperator: 13.792ms
相同的逻辑,三倍耗时,性能差距如此明显!
做加法很简单吗?
我们从幼儿园开始就会用"+"了,但是在js里,做加法绝对不是一件简单的事情。我们去ES规范看看:
做个加法竟然有这么多步骤,而且每个步骤又会引发一系列的流程,比如ToPrimitive
ToString
ToNumeric
和一系列的判断。
比如这个ToPrimitive
,背后又是一大段逻辑:
规范太繁琐懒得看也没关系,你只要知道,哪怕是简单的加法操作,对于底层来说,也不是一件简单的事情,而且在类型判断
与类型转换
上占用了大量时间。(所以说动态语言性能不好...
TurboFan、推测优化
V8执行js代码可以简化为以下几个步骤:
分词拆成token - 解析成ast - Ignition解释为字节码 - 机器码
而V8中存在一个名为TurboFan
的优化编译器,用来将热点代码编译为高效的机器码,下次再遇上热点代码,直接调用编译完毕的机器码即可,这样就能达到媲美编译型语言的高性能。以我们编写的sum
函数为例:
-
解释器执行:函数
sum
最初通过Ignition解释器执行,解释器会记录调用sum
函数的次数。 -
内联缓存:记录参数
a
和b
的类型(比如它们是Number)。 -
执行计数累积:每次调用
sum
函数时,执行计数增加。 -
阈值触发编译:当调用次数达到阈值时,TurboFan会将
sum
函数标记为热点代码。 -
类型反馈分析:TurboFan使用收集到的类型信息,推断
a
和b
总是整数,并生成优化后的机器码,这段机器码会跳过我们在ES标准中看到的加号操作所引发的一系列复杂流程,因为已经被推断为Number了,类型相关的流程就不需要了。 -
热路径优化:内联
sum
函数,去掉解释器开销,并应用进一步优化(如常量折叠、寄存器分配等)。
V8的优化策略完全是基于历史信息推测而来的,优化的结果是高度优化的机器码,但是在我们的第二版函数additionOperator
的例子中,由于调用的时候参数类型变化的极其频繁,导致引擎无法推测优化,只能走完加法的全部流程,并且还要走解释器,造成了极大的性能开销。
如何编写高性能代码
其实性能优化逃不开一个定律:越静态的越好优化,越动态的越难优化。譬如编译型语言对上解释型语言,Vue的模板对上jsx,esmodule对上commonjs。不仅如此,我们在业务开发时,越稳定的业务的代码质量也会越高(所以产品经理才是一个项目的灵魂人物)。
扯远了,对于我们日常写代码来说,我们要保证变量、参数、函数的返回值的形态与结��尽量不变。
转载自:https://juejin.cn/post/7384740010896392211