为什么都说 javascript 慢
我们都知道 javascript 有两大特点:一是弱类型,二是解释型。
弱类型
什么是弱类型语言呢,一句话就是类型的概念,在语言中很弱,你感受不到他的存在。js 就是这样,你在编码的时候不需要显示指定变量的类型,也无需关注变量的类型,一个变量的类型是在代码运行到具体的某一行的那一刻才确定的,在编码的过程中,你可以在任何时候更改变量的类型,例如 js 中的 var,php 中的 $。
解释型
什么是解释型语言呢,就是代码在运行的时候,由【解释器】直接去读源码,然后将源码翻译成对应的机器码,最后交由 CPU 来执行。这样就造成每一行代码的执行,都需要先进行【源码到机器码】的翻译,然后再给CPU执行,这样的效率明显非常低。
机器码
机器码(machine code),学名机器语言指令
,有时也被称为原生码(Native Code)
,是电脑CPU可以直接执行的低级语言,它通常表现为一系列的二进制代码。这些代码能被计算机硬件直接理解和执行,用于指示计算机进行各种操作
通常意义上来理解的话,机器码就是计算机可以直接执行,并且执行速度最快的代码。
强类型
与弱类型相对,强类型语言要求我们在编码的时候,必须指定变量的类型,并且类型一旦指定,就不可再次更改,如Java中的整型int,浮点型float等。
编译型
与解释型语言不同,编译型语言在源代码开发完成之后,会经过编译器将源码编译成【可执行程序】,然后才可以运行。由于这个编译过程并没有时间要求,所以编译器可以对源码进行长时间的大量的优化措施。最后的结果就是编译型语言最后生成的可执行程序是经过大量长时间优化后,可以直接被机器执行的程序,不需要像解释型语言那样,在运行的时候才将代码翻译成机器码,再给机器执行,并且这个过程还没有时间对代码优化,在机制上就导致了解释型语言的执行速度不如编译型语言。
可执行程序
我们知道机器硬件只能识别机器指令,并不识别源代码,所以对于编译型语言我们需要编译器将源代码编译成机器码,才可以执行,但是编译出来的结果我们需要保存下来,保存下来的这个文件就是可执行程序,这样我们再次使用的时候,直接执行可执行程序即可,无需再次编译,这个文件在 window 通常 .exe
后缀。
机器码当然是可执行程序的核心部分,但可执行程序并不仅是机器码,它还包括程序运行所需的各种资源和信息,如:
- 程序的入口点:标识程序开始执行的地方。
- 静态数据:程序中使用的固定数据,如字符串常量等。
- 动态链接库(DLLs)的引用:如果程序依赖于外部库或框架,这些信息也会包含在可执行文件中。
- 重定位信息:允许程序代码在内存中被加载到不同的地址空间运行。
- 调试信息:虽然主要用于开发阶段,但有些程序在发布时也可能包含调试信息,以便于后续的错误分析和修复。
强类型 VS 弱类型
上面我们说了强类型与弱类型的区别,看上去,对性能似乎并没有什么影响,实际上却影响很大。
概括来说就是,静态类型语言在编译后会大量利用类型已知的优势,比如int类型,占用4个字节,编译后的代码就可以使用内存地址加偏移量的方法存取变量。而地址+偏移量的算法汇编非常容易实现。
那动态类型语言是如何做的呢?概括的来说就是当做字符串通通存下来,之后存取就用字符串匹配。
编译型 VS 解释型
上面我们说了,编译型与解释型的区别,并且也了解到为什么编译型性能更高。
那为什么还会出现解释型的语言呢?
我们上面说了,像 js代码(高级语言) 并不能直接被机器执行,而是需要 js 引擎将源代码翻译成机器语言让机器来执行。
而编译型语言同样是在开发完成之后,需要由编译器将源代码编译为【可执行程序】;而可执行程序的核心内容为机器码,但是对于不同的CPU架构,机器码并不相互识别,这就导致编译型语言的跨平台性并不那么好,即使CPU架构一致,但是可执行程序还包含一系列其他资源和信息,例如程序入口、静态数据、系统API调用等,这些系统层面的差异,这就导致编译型语言基本很难跨平台。
java 既有编译过程,又有解释过程
为了有更好性能,同时又不丧失跨平台性。那怎么做呢,java 先将源代码编译成中间代码(字节码),在编译成字节码的这个过程中,对源码进行大量的分析优化措施。然后由 java 虚拟机来解释执行优化后的字节码,这就使得,java 编译后的字节码拥有了良好的跨平台性,同时字节码经过大量优化,性能比编译型弱些,但也比纯解释型要高。
所以就形成了C/C++是大哥,Java是二哥,一群解释型脚本语言是小弟们。大哥,独孤求败。二哥,想法子和大哥站在一条线上。小弟们,尽全力跟上二哥。
现代javascript引擎的努力
那java想要继续提高性能,肯定得是继续优化虚拟机解释执行字节码的速度,这儿正是和大哥拉开差距的地方。从大哥那学了很多招。其中重要的一招就是JIT(Just-In-Time),主要的思想就是解释器在解释字节码的时候,会将部分字节码转化成本地代码(汇编代码),这样可以被CPU直接执行,而不是解释执行,从而极大地提高性能。
那同样的目的,javascript 想要继续提高性能,似乎也可以使用这样的思想。
在继续描述之前我们需要先了解下解释器与编译器
解释器与编译器
我们上面说了编译型语言和解释型语言的区别,编译型语言是把源代码编译成目标代码,执行时不需要再次编译,直接在目标平台上运行,执行编译的这个工具,我们就称它为编译器。
解释型语言,是在运行的时候将源码代一行一行的解释成可以直接在目标平台上运行的代码然后执行,执行解释的这个工具,我们就称它为解释器。
解释器的利弊
解释器没有编译过程,它启动会更快,同时你不需要等待编译过程完成就可以运行你的代码。
基于此,解释器更契合 javascript,对于 Web 开发人员来讲,可以快速执行代码并看到结果,所见即所得是非常重要的,这也是为什么最开始浏览器都是用 javascript 解释器的原因。
但如果你运行同样的代码一次以上的时候,解释器的弊端就出来了,例如一个循环,那同样的代码,你循环了10次,解释器就需要解释10次,这效率一下子就拉了。
编译器的利弊
与解释器正好相反,它必须花费一定的时间,将代码编译成可执行程序,然后才可以在机器上执行。但同样的例子,对于循环,它可以执行的非常快,因为它不需要一遍一遍的去翻译源代码。
JIT(Just-in-time)
那现代 javascript 引擎做了什么呢,同样引入一个编译过程,但这个编译过程和运行时一起的,核心思想是在 javascript 引擎中增加一个监视器(也叫分析器),监视器监控着代码的运行状况,记录代码一共运行了多少次、如何运行等信息。
如果同一行代码运行了好几次,那这段代码就被标记为“warm”,如果运行了非常多次,则标记为“hot”
warm
如果一段代码变为了“warm”,那 JIT 就将这段代码送到编译器去编译,同时将编译结果存储起来。
如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。通过这样的做法可以加快执行速度。这相当于再次执行同样的代码的时候,不需要再进行解释了。
但编译器还可以找到更有效的执行代码的方法,也就是对编译结果继续优化,但对于“warm”,编译器也不能优化太久,因为它执行的次数并不是非常多,在它上面还有一个“hot”,优化太久会使程序的执行在这里 hold 住。
hot
但对于“hot”,代码执行次数非常多,也就是说几乎所有的执行时间都耗费在这里,那花点时间做优化,也是值得的。
所以如果一段代码别标记成了“hot”,那JIT就会将其发送到优化编译器,生成一个更快速更高效的代码版本出来,然后将其存储下来。
如此一来,对于仅执行一次的代码,直接解释执行,不需要消耗资源对他进行优化。对于执行多次的代码,我们也进行了编译优化,可以做到两者兼得