上一篇 我们讨论了跳转指令,并实现「正跳转」的翻译,但最终困在「负跳转」上。而且,由于线程模型的差异,我们不能 1:1 的翻译,必须对流程进行一些改造。

当初之所以选择翻译,而不是模拟,就是出于性能考虑。但是,这并不意味绝对不能用模拟 —— 如果能用少量模拟,解决一些技术障碍,那也是值得的。

现在,我们尝试用模拟的方式,来控制流程。

流程模拟

JS 的流程控制,就好比排队:如果你不想排了,可以随时退出,大家都不介意;但是之后又想回来插队,这就不行了,你必须从头排起。所以,只能退出,不能插入。

插入就类似指令跳入,所以得尽量避开。我们以跳入点(下图带箭头的)为界线,将指令分割成多块:

                        -------------
XXX 1 | block 0 |
JXX L2 --. | |
XXX 2 | | |
L1: | <-. ~~~~~~~~~~~~~~~~~~~
XXX 3 | | | block 1 |
XXX 4 | | | |
L2: <-| | ~~~~~~~~~~~~~~~~~~~
XXX 5 | | block 2 |
XXX 6 | | |
JXX L1 --| | |
XXX 7 -------------

这样,每个块中就没有跳入点了,满足 JS 的需求。现在要做的,就是块与块之间的控制。

我们先把每个块,包裹成单独的 function:

function block_0() {
XXX 1
... // JXX L2
XXX 2
} function block_1() {
XXX 3
XXX 4
} function block_2() {
XXX 5
XXX 6
... // JXX L1
XXX 7
}

为什么要用函数?因为函数可以赋给变量。通过函数变量,可灵活操控流程:

var nextFn = block_0;   // 用这个变量,模拟流程控制

function block_0() {
XXX 1
if (...) { // JXX L2
nextFn = block_2;
return;
}
XXX 2
nextFn = block_1 // 默认下一块
} function block_1() {
XXX 3
XXX 4
nextFn = block_2 // 默认下一块
} function block_2() {
XXX 5
XXX 6
if (...) { // JXX L1
nextFn = block_1;
return;
}
XXX 7
nextFn = null // end
}

这样,就能通过 nextFn 控制流程了。我们用一个简单的状态机,就能驱动这些指令块:

while (nextFn) {
nextFn();
}

这样,就解决跳转问题了!

对于普通指令,仍然是 1:1 的翻译;只有跳转时,才会用这种方式。因此总体效率仍然很高。

事实上,这其中还可以再优化 —— 我们只切割负跳转,正跳转尽可能使用 do..while(0) 来实现,因此还可以再减少分割的块数。

时钟频率

使用现在这种方案,线程模型的问题自然也可以解决了。

我们不再用死循环,而是用定时器去驱动,每一桢执行若干个块,这样就不会卡死浏览器了:

setInterval(function() {
nextFn();
nextFn();
...
}, 20);

那么,每一桢究竟执行多少次 nextFn 呢?这就看模拟的 CPU 时钟频率了。

假设以 1MHz 的频率运行,也就是每秒 100 万个周期。脚本定时器每秒触发 50 桢,那么每一帧得跑 2 万个周期:

setInterval(function() {
cycle_remain = 20000; while (cycle_remain > 0) {
nextFn();
}
}, 20);

每个指令的周期,基本都是固定的,可以查考文档。所以翻译时,每个流程会消耗多少周期,也是确定的。例如:

function block_1() {
...
if (...) {
nextFn = ...
cycle_remain -= 8 // 在此跳出,本流程消耗 8 周期
return
}
...
cycle_remain -= 12 // 运行到此,本流程消耗 12 周期
}

当周期用尽时,就完成这一桢的工作,等待下一帧触发。这样就不至于 100% 占用的浏览器资源,有充足的时间处理输入和输出。

死循环的问题,就这样解决了。

降低误差

为了提高性能,这里只在流程切换时,才会判断周期;而不是传统模拟器那样,每执行一个指令判断一次。

当然,这么做会有些误差:最后一个流程执行完,cycle_remain 不一定正好等于 0。事实上大多时候都是负数(超标),于是导致速度偏快。

为了消除误差,我们可以把超标的数量,在下一帧里扣除。例如本次 cycle_remain 最终是 -100,那么下一桢的 cycle_remain 则初始化为 19900。

这样,虽然每一帧有些波动,但在宏观上,还是相对稳定的。


思考题

回到上一章。假设 跳转指令能 1:1 翻译成 JS,那么我们就用不着分割流程了。这时是否能用一些其他技术,解决时钟频率的问题?

其实在如今 ES6 版 JS 中,又新增了一个强大的流程控制武器 —— yield。

yield 允许在函数任意位置跳出,之后可以回到那个位置,继续运行。

因此,可在负跳转之前判断周期,如果超了,则 yield 出去,让浏览器有喘息的机会:

function* routine() {
L1: do {
...
if (...) { // JXX L1
if (cycle_remain < 0) yield
continue L1
}
...
} while (0)
}

等到休息完成,还可以回到上次的位置继续工作。

这看起来十分好用!但是我们没用它。并非兼容性问题,也不是性能问题,而是:这一节的前提纯属 假设

结尾

这一篇和前一篇,我们讨论的只是「静态跳转」的情况。但事实上 6502 指令集还支持「动态跳转」,这又该如何实现?

下一篇,我们讨论如何处理动态跳转。

机器指令翻译成 JavaScript —— No.3 流程分割的更多相关文章

  1. 【探索】机器指令翻译成 JavaScript

    前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...

  2. 机器指令翻译成 JavaScript —— No.5 指令变化

    上一篇,我们通过内置解释器的方案,解决任意跳转的问题.同时,也提到另一个问题:如果指令发生变化,又该如何应对. 指令自改 如果指令加载到 RAM 中,那就和普通数据一样,也是可以随意修改的.然而,对应 ...

  3. 机器指令翻译成 JavaScript —— No.6 深度优化

    第一篇 中我们曾提到,JavaScript 最终还得经过浏览器来解析.因此可以把一些优化工作,交给脚本引擎来完成. 现代浏览器的优化能力确实很强,但是,运行时的优化终归是有限的.如果能在事先实现,则可 ...

  4. 机器指令翻译成 JavaScript —— No.7 过渡语言

    上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言.现在我们来研究一下,将 6502 指令转换成 C 的可行性. 跳转支持 翻译成 C 语言,可比 JS 容易多了.因为 C 支持 ...

  5. 机器指令翻译成 JavaScript —— 终极目标

    上一篇,我们顺利将 6502 指令翻译成 C 代码,并演示了一个案例. 现在,我们来完成最后的目标 -- 转换成 JavaScript. 中间码输出 我们之所以选择 C,就是为了使用 LLVM.现在来 ...

  6. 机器指令翻译成 JavaScript —— No.2 跳转处理

    上一篇,我们发现大多数 6502 指令都可以直接 1:1 翻译成 JS 代码,但除了「跳转指令」. 跳转指令,分无条件跳转.条件跳转.从另一个角度,也可分: 静态跳转:目标地址已知 动态跳转:目标地址 ...

  7. 机器指令翻译成 JavaScript —— No.4 动态跳转

    上一篇,我们用模拟流程的方式,解决了跳转问题. 不过静态跳转,好歹事先是知道来龙去脉的.而动态跳转,只有运行时才知道要去哪.既然流程都是未知的,翻译从何谈起? 动态跳转,平时出现的多吗?非常多!除了 ...

  8. 四十年前的 6502 CPU 指令翻译成 JS 代码会是怎样

    去年折腾的一个东西,之前 blog 里也写过,不过那时边琢磨边写,所以比较杂乱,现在简单完整地讲解一下. 前言 当时看到一本虚拟机相关的书,正好又在想 JS 混淆相关的事,无意中冒出个问题:能不能把某 ...

  9. [书籍翻译] 《JavaScript并发编程》第六章 实用的并发

    本文是我翻译<JavaScript Concurrency>书籍的第六章 实用的并发,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript ...

随机推荐

  1. Restful资源文章

    理解RESTful架构 RESTful API设计指南 RESTful架构详解 NodeJs的RESTful API

  2. 查看w3wp进程占用的内存及.NET内存泄露,死锁分析

    一 基础知识 在分析之前,先上一张图: 从上面可以看到,这个w3wp进程占用了376M内存,启动了54个线程. 在使用windbg查看之前,看到的进程含有 *32 字样,意思是在64位机器上已32位方 ...

  3. Pivot 和 Unpivot

    在TSQL中,使用Pivot和Unpivot运算符将一个关系表转换成另外一个关系表,两个命令实现的操作是“相反”的,但是,pivot之后,不能通过unpivot将数据还原.这两个运算符的操作数比较复杂 ...

  4. Carousel 旋转画廊特效的疑难杂症

    疑难杂症 该画廊特效的特点就是前后元素有层级关系. 我想很多人应该看过或者用过这个插件carousel.js,网上也有相关的教程.不知道这个插件的原型是哪个,有知道的朋友可以告诉我. 该插件相对完美, ...

  5. 微信小程序(微信应用号)组件讲解

    这篇文章主要讲解微信小程序的组件. 首先,讲解新建项目.现在有句话:招聘三天以上微信小程序开发,这个估计只能去挖微信的工程师了.技术新,既然讲解,那我们就从开始建项目讲解. 打开微信web开发者工具, ...

  6. java中易错点(二)

    java,exe是java虚拟机 javadoc.exe用来制作java文档 jdb.exe是java的调试器 javaprof,exe是剖析工具 解析一: sleep是线程类(Thread)的方法, ...

  7. 跟着老男孩教育学Python开发【第二篇】:Python基本数据类型

    运算符 设定:a=10,b=20 . 算数运算 2.比较运算 3.赋值运算 4.逻辑运算 5.成员运算 基本数据类型 1.数字 int(整型) 在32位机器上,整数的位数为32位,取值范围为-2**3 ...

  8. centos6和centos7防火墙的关闭

    CentOS6.5查看防火墙的状态: [zh@localhost ~]$service iptable status 显示结果: [zh@localhost ~]$service iptable st ...

  9. Linux下高cpu解决方案

    昨天搞定了一个十万火急的issue,客户抱怨产品升级后系统会变慢和CPU使用率相当高,客户脾气很大,声称不尽快解决这个问题就退货,弄得我们 R&D压力很大,解决这个issue的任务分给了我,客 ...

  10. openfire的组件(Component)开发

    在之前的文章<Openfire阶段实践总结>中提到过一种openfire的扩展模式Compoent.本文将主要探讨对这种模式的应用与开发方法. 内部与外部组件介绍 在openfire中的许 ...