上一篇 我们讨论了跳转指令,并实现「正跳转」的翻译,但最终困在「负跳转」上。而且,由于线程模型的差异,我们不能 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. DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

    写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...

  2. SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=》提升)

     SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=>提升,5个Demo贯彻全篇,感兴趣的玩才是真的学) 官方demo:http://www.asp.net/si ...

  3. 代码的坏味道(14)——重复代码(Duplicate Code)

    坏味道--重复代码(Duplicate Code) 重复代码堪称为代码坏味道之首.消除重复代码总是有利无害的. 特征 两个代码片段看上去几乎一样. 问题原因 重复代码通常发生在多个程序员同时在同一程序 ...

  4. PHP 设计模式概述

    一.设计模式(Design pattern)是什么? 设计模式是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. ...

  5. JavaScript学习笔记(三)——this、原型、javascript面向对象

    一.this 在JavaScript中this表示:谁调用它,this就是谁. JavaScript是由对象组成的,一切皆为对象,万物皆为对象.this是一个动态的对象,根据调用的对象不同而发生变化, ...

  6. Android—关于自定义对话框的工具类

    开发中有很多地方会用到自定义对话框,为了避免不必要的城府代码,在此总结出一个工具类. 弹出对话框的地方很多,但是都大同小异,不同无非就是提示内容或者图片不同,下面这个类是将提示内容和图片放到了自定义函 ...

  7. 跟着老男孩教育学Python开发【第一篇】:初识Python

    Python简介 Python前世今生 Python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解 ...

  8. python selenium

    https://segmentfault.com/a/1190000007249396?_ea=1293878

  9. ASP.NET Linux部署(2) - MS Owin + WebApi + Mono + Jexus

    ASP.NET Linux部署(2) - MS Owin + WebApi + Mono + Jexus 本文承接我的上一篇博文: ASP.NET 5 Linux部署,那篇文章主要是针对最新的ASP. ...

  10. Lesson 22 A glass envolops

    Text My daughter, Jane, never dreamed of receiving a letter from a girl of her own age in Holland. L ...