【磨破嘴皮】Just In Time (JIT)

Javascript是一种解释器逐行翻译执行的语言,遵循JIT范式

  • 源代码通过词法分析和语法分析解析为抽象语法树(AST)
  • 抽象语法树通过解释器生成字节码
  • 字节码通过编译器将部分常用的字节码替换为高度优化的机器码

解释器与编译器

解释器

简单,启动快,执行慢,原因在于解释器不能识别重复的工作,同一行代码,会重复的翻译执行

编译器

复杂,启动慢,执行快,他会在执行前翻译所有代码,并做优化,比如找出重复的代码,进行共享

即时编译 JIT

他结合了解释器与编译器的优点,使得代码的转换和执行都变得更快

在执行代码期间,监视器(探查器)会标记热代码片段和运行很多次的热代码片段,随着次数增加,会标记代码块的热度,并根据等级分别发送给基线编译器或优化编译器

  • Warm 将解释器执行代码发送给jit引擎,将其编译为机器码,但此处不会
  • hot 解释执行代码被替换为warm编译出的机器码
  • Very hot 将解释器执行的代码发送给优化编译器,创建和编译出更高效的机器码执行代码并进行替换

基线编译器

warm部分的代码将被编译成字节码,并尝试通过创建存根来优化代码,以下面代码为例子(真实环境下,这段代码过于简单会被直接编译成机器码)

javascript 复制代码
function concat(arr) {
    let res = '';
    for(let i = 0; i < arr.length; i++) {
        res+=arr[i]
    }

    return res
}
console.log(concat(['a', 1,'b', true, 'c']))

基线编译器会将res += arr[i]转换为存根,但由于此指令是多态的(没有什么能保证i每次都是一个整数或arr[i]每次都为一个字符串),它将为每个可能的组合创建存根(可以理解为)。

  • i是一个整数?
  • res是一个数组?
  • arr是一个数组?
  • arr[i]是一个字符串?

优化编译器

优化编译器会将基线编译器创建的存根变成一个组(可以简单的理解为针对数组内不同的值创建了对应的机器码方法称之为存根组,而存根则是每一步代码的匹配对应的机器码),并将上面的一些例子固化,匹配到了就直接使用优化后的高度优化的机器码,跳过判断的流程。

  • arr是一个数组?是
  • i是一个整数?是
  • res是一个字符串?是

编译器去优化

以上面代码为例子如果这里的数组中出现了一个object类型,会导致优化编译器的存根组失效,执行去优化操作,优化和去优化的过程是昂贵的。由此产生了一类JavaScript的优化方法,下面将详细描述。

如何优化Js代码

在构造函数中声明对象属性

更改对象属性会产生新的隐藏类

ini 复制代码
class Point {
 constructor(x, y) {
   this.x = x;
   this.y = y;
}
}
var p1 = new Point(11, 22);  // hidden class Point created
var p2 = new Point(33, 44);
p1.z = 55;  // another hidden class Point created

当p1中添加了z属性时,v8会单独为p1创建一个新的隐藏类,此时,任何接受Point对象的方法都将发生去优化

保持对象属性不变

更改对象属性的顺序会导致新的隐藏类,因为对象形状中是包含顺序的。

ini 复制代码
const a1 = { a: 1 };  # hidden class a1 created
a1.b = 3;
const a2 = { b: 3 };  # different hidden class a2 created
a2.a = 1;

上面的代码中,a1和a2有不同的隐藏类。修复顺序允许编译器重用同一个隐藏类。因为添加的字段(包括顺序)用于生成隐藏类的id

修复函数参数类型

函数根据特定参数位置的值类型更改对象形状。如果此类型发生更改,则函数将去优化并重新优化。

scss 复制代码
function add(x, y) {
 return x + y
}
add(1, 2);  
add("a", "b"); 
add(true, false);
add([], []);
add({}, {}); 

第9行过后,V8将不会再优化add这个函数。

在脚本 作用域 中声明类

不要在函数作用域中声明类。以下面这个例子为例:

javascript 复制代码
function createPoint(x, y) {
 class Point {
   constructor(x, y) {
     this.x = x;
     this.y = y;
  }
}
 return new Point(x, y);
}
function length(point) {
 ...
}

每一次createPoint这个函数被调用的时候,一个新的Point原型会被创建。

每一个新的原型都对应着一个新的对象形状,所以每一次length函数都会看到一个新的point的对象形状。

跟之前一样,当看到4个不同的对象形状的时候,函数会变得megamorphic,TurboFan将不会再尝试优化length函数。

在脚本作用域中声明class Point,我们可以避免每一次调用createPoint的时候,生成不同的对象形状。

使用for in

for...in循环比函数迭代、带箭头函数的函数迭代和for循环中的object.keys快4-6倍。

无关字符不影响性能

无关的字符,比如空白,注释,变量名长度,函数签名等,不会影响函数的性能

Try/catch/finally 不是毁灭性的

Try代码块以前容易出现高昂的优化-去优化的周期

总结

优化的方法在于减少对已有对象的修改,以及函数参数的改变

参考

相关推荐
程序员码歌1 小时前
短思考第261天,浪费时间的十个低效行为,看看你中了几个?
前端·ai编程
Swift社区2 小时前
React Navigation 生命周期完整心智模型
前端·react.js·前端框架
若梦plus2 小时前
从微信公众号&小程序的SDK剖析JSBridge
前端
用泥种荷花3 小时前
Python环境安装
前端
Light603 小时前
性能提升 60%:前端性能优化终极指南
前端·性能优化·图片压缩·渲染优化·按需拆包·边缘缓存·ai 自动化
Jimmy3 小时前
年终总结 - 2025 故事集
前端·后端·程序员
烛阴3 小时前
C# 正则表达式(2):Regex 基础语法与常用 API 全解析
前端·正则表达式·c#
roman_日积跬步-终至千里3 小时前
【人工智能导论】02-搜索-高级搜索策略探索篇:从约束满足到博弈搜索
java·前端·人工智能
GIS之路3 小时前
GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据
前端
多看书少吃饭3 小时前
从Vue到Nuxt.js
前端·javascript·vue.js