likes
comments
collection
share

我翻了ES规范,发现用Typescript写的代码性能更强

作者站长头像
站长
· 阅读数 21

不就是个加法

你有一个求和函数:

function sum(a, b) {
    return a + b
}

sum(1, 2) // 3

某天你又有了一个字符串拼接的需求,你又写了这么一个函数:

function concatenate(a, b) {
    return a + b
}

concatenate('1', '2') // '12'

此时你发现,sumconcatenate这两个函数除了名字之外,一模一样,于是你灵机一动,改造成了这样:

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规范看看:

我翻了ES规范,发现用Typescript写的代码性能更强

做个加法竟然有这么多步骤,而且每个步骤又会引发一系列的流程,比如ToPrimitive ToString ToNumeric和一系列的判断。

比如这个ToPrimitive,背后又是一大段逻辑:

我翻了ES规范,发现用Typescript写的代码性能更强

规范太繁琐懒得看也没关系,你只要知道,哪怕是简单的加法操作,对于底层来说,也不是一件简单的事情,而且在类型判断类型转换上占用了大量时间。(所以说动态语言性能不好...

TurboFan、推测优化

V8执行js代码可以简化为以下几个步骤:

分词拆成token - 解析成ast - Ignition解释为字节码 - 机器码

而V8中存在一个名为TurboFan的优化编译器,用来将热点代码编译为高效的机器码,下次再遇上热点代码,直接调用编译完毕的机器码即可,这样就能达到媲美编译型语言的高性能。以我们编写的sum函数为例:

  • 解释器执行:函数sum最初通过Ignition解释器执行,解释器会记录调用sum函数的次数。

  • 内联缓存:记录参数ab的类型(比如它们是Number)。

  • 执行计数累积:每次调用sum函数时,执行计数增加。

  • 阈值触发编译:当调用次数达到阈值时,TurboFan会将sum函数标记为热点代码。

  • 类型反馈分析:TurboFan使用收集到的类型信息,推断ab总是整数,并生成优化后的机器码,这段机器码会跳过我们在ES标准中看到的加号操作所引发的一系列复杂流程,因为已经被推断为Number了,类型相关的流程就不需要了

  • 热路径优化:内联sum函数,去掉解释器开销,并应用进一步优化(如常量折叠、寄存器分配等)。

V8的优化策略完全是基于历史信息推测而来的,优化的结果是高度优化的机器码,但是在我们的第二版函数additionOperator的例子中,由于调用的时候参数类型变化的极其频繁,导致引擎无法推测优化,只能走完加法的全部流程,并且还要走解释器,造成了极大的性能开销。

如何编写高性能代码

其实性能优化逃不开一个定律:越静态的越好优化,越动态的越难优化。譬如编译型语言对上解释型语言,Vue的模板对上jsx,esmodule对上commonjs。不仅如此,我们在业务开发时,越稳定的业务的代码质量也会越高(所以产品经理才是一个项目的灵魂人物)。

扯远了,对于我们日常写代码来说,我们要保证变量参数函数的返回值的形态与结��尽量不变。

转载自:https://juejin.cn/post/7384740010896392211
评论
请登录