解释执行为何比编译执行慢? 入眼就看到一群人振振有词的在这缠杂不清…… 其实这类问题有一个简单粗暴的解决思路,建议不想当桃谷六仙的跟着学学。 这个思路就是——极端化,极限化。 什么叫极端化、极限化? 极端化、极限化的意思,就是把问题尽量简化、把解决方案也尽量简化,直到简无可简——那么这时候,我们就可以给出一个结论“解释执行的效率上限是xxxx”,清清楚楚明明白白,就不用桃谷六仙一样缠杂不清了。 具体到“解释执行为何比编译执行慢”这件事上,我们不妨也给它极端化、极限化一下。 怎么极端化、极限化呢? 让我们想象一种特殊的、解释执行的语言;这种语言当然不是x86机器码(不然CPU直接就能运行,就不好腆着脸叫它“解释执行”了),但它和x86机器码是一一对应关系。 也就是说,x86有的每个指令,这种语言都有;x86没有的指令,这种语言也没有。 我们还可以要求,这种语言的指令都是数字,这样可以省去字符串解析需要的时间;另外,我们可以要求这种语言的寄存器、指令执行逻辑都和x86指令集兼容——这样就不需要复杂的翻译工作了。 好了,现在,当我们拿到这样一种语言写的程序、想要让x86CPU解释执行它,我们需要做什么? 没错,查表。 我们可以按顺序读入这种语言写的程序的每一条指令,然后查表转换到x86机器码——前面说过,两者一一对应。 那么,怎么查表呢? 三个方案。方案一是哈希表,方案二是二分法查找——受评论区提示,增加方案三:以空间换时间,直接用指令码当跳转地址,跳对应指令处执行。 对方案一,执行一条指令时,必需的操作是:
可见,哪怕耍赖到极限,方案一也需要起码10个时钟周期(考虑跳转本身的比较可能更多)。 对方案二,执行一条指令时,必需的操作和hash表方案差不多;但需要 \log_{2}{N} 次搜索(N是x86指令条数),才能找到对应的指令。因此总的执行时间是只多不少的。 对方案三,大概是这样:
不考虑缓存未命中等问题,5~6个时钟周期。 综上,可见,解释执行哪怕优(耍)化(赖)到极致,它也至少需要5~10倍的性能消耗。 换句话说,解释执行至少要比直接执行编译后的程序慢5~10倍! 这个“解释执行至少比直接执行多消耗10倍时间”的结论其实来自国外一位学者的研究论文,但我没具体看过——所以,我随便搞出来的这个论证思路有效,但肯定不严谨。比如没有考虑“高级语言指令可以翻译成一组机器指令”、于是一次打包处理效率可以更高之类情况(这的确可以提高解释执行效率;但这条线是无法无限发展的。因为这会引入语法分析等动作,结果是得不偿失的:所以Java才会搞一个几乎和机器指令对应的字节码,解释执行字节码而不是高级语言代码)。 总之,“解释执行至少比直接执行多消耗10倍时间”这个结论是至今未曾受到挑战的——熟悉街机模拟器、ps2/3模拟器的小伙伴肯定知道我在说什么。 当然,如你所见,上面这个讨论是在设置了一大堆奇葩先决条件的基础上来的;现实中是不存在这么理想的解释性语言的——除非你故意玩梗。 实践中,常见的情况是: 1、在任意CPU上(通过虚拟机)解释执行精心优化的、极端理想的“字节码”(如传说中极端牛逼的Java字节码) 2、在x86 CPU上解释执行arm平台的可执行程序(如VMware/virtual box等虚拟机) 3、在x86 CPU上解释执行其它异构平台的可执行程序(典型如街机模拟器) 4、在任意CPU上解释执行通常水平的字节码(python字节码等) 5、在任意CPU上解释执行高级语言程序(如VBA、perl、php等) 自上至下,执行效率越来越差。 其中,1要求精心设计字节码,最好能使得一条字节码平均相当于10条以上物理机器指令,这才能把执行需要的额外时间压缩到2倍(不考虑缓存未命中以及分支预测失败消耗)——但,无论如何这个额外消耗是逃不掉的;尤其是,高级语言指令到物理机器指令,平均来说根本不可能对应十条机器指令。 如上,我们可以比对Java到字节码和c++到x86指令,可见Java根本做不到“一条字节码对应多条x86机器码”。 因此,虽然我很理想化的把Java字节码的效率排在“虚拟机执行其它CPU上面的程序”之前;但很遗憾,其实它并不能证明它就配坐在这个位置。 换句话说,除非直接调用预编译的东西作弊、或者动用jit;否则,一行行解释执行的话,这段Java程序的执行时间消耗起码是c++的10倍;如果考虑来回跳转引起的缓存未命中、分支预测失效等等问题,这个差距只会更大——并不会比你在PC上用Android模拟器跑Android app高明。 尤其到了高级语言程序,每个标识符、每条指令都是个字符串,读入字符串这个操作本身就需要多个时钟周期(x86的movs指令并不是每个平台都有的);更不要说什么语法分析、生成语法树、翻译然后才能执行了——这些复杂的动作,使得哪怕高级语言一条指令可以翻译成10条以上的机器指令,最终效率也远低于虚拟机执行同构平台上面的可执行程序。 当然,类似xen、kvm等半虚拟化、硬件虚拟化在x86cpu上执行x86指令集是另一种情况,可以达到物理机95%以上效能:但这已经不是解释执行了。 当然,如你所见,上面的c++示例代码到机器指令,指令条数比绝对到不了10——加上保存现场、参数传递、恢复现场、返回等等,3行c++代码最终转换成了8条机器指令。这是远远远远抵消不了文本载入、语法分析等等消耗的:这就是为什么解释性语言后来纷纷搞字节码、而不是直接分析“信息损失最少”的源代码的原因。 事实上,熟悉C/C++的程序员都知道,较长的C/C++程序编译到汇编之后,长度并不会增加很多;因为编译器会自动执行很多优化、从而使得必须执行的指令数远少于预期——这甚至会给懂汇编的程序员分析代码问题带来不便。所以编译器专门给了个-O0选项来禁止优化,从而使得高级语言代码可以和机器指令相互对应。 正因为这道“10倍性能消耗”墙卡的死死的,Java才不得不上了JIT,也就是即地编译(Just In Time Compilation)——宁可付出巨大代价、在执行期执行程序编译过程,都不能再走解释执行的路子。 原因很简单,解释执行——此路不通。 附录: 各种编程语言的执行速度比较(删除旧的,以评论区 同学更科学的测试为准): 原测试没有给Java等jit语言“预热”时间,优化也不够到位,使得Java等语言耗用的时间达到c/c++的两倍左右;新的测试去除了这些步骤,仅测试执行速度——此时只要优化过关,那么jit语言其实也是编译型语言,速度自然可以提到极限。 最后的Python是解释执行,拿它和编译型语言比,结果就近乎公开处刑了…… |
|