原文链接:https://darksi.de/d.sea-of-nodes/

简介

这篇文章将讲述我最近学到的Sea of nodes编译器概念。

尽管不是完全必要,但在阅读本文之前,可以先看一下我以前在JIT编译器上发表的一些文章,应该会很有一些帮助:

  • How to start JIT-ting
  • Allocating numbers
  • SMIs and Doubles
  • Deoptimize me not, v8

编译器=翻译器

编译器是软件工程师每天都要使用的工具。令人惊讶的是,即使是那些认为自己不会编写代码的人,在一天当中仍然会大量使用编译器。这是因为大多数Web页都依赖于客户端代码的执行,并且很多这样的客户端程序都以源代码的形式传递给浏览器,例如javascript。

在这里,我们要讨论一件重要的事:尽管源代码(通常)是人类可读的,但对于您的笔记本电脑/计算机/手机/ ...的CPU来说,它几乎是垃圾。 另一方面,计算机可以读取的机器代码几乎总是人类难以阅读的。 我们必须要做一些事情来处理它,此问题的解决方案称为翻译的过程。

很少有编译器只执行一次翻译:就从源代码直接到机器代码。 在实践中,大多数编译器至少要经过两次翻译过程:从源代码到抽象语法树(AST),从AST到机器码。 在这种情况下,AST的作用类似于中间表示(IR),顾名思义,AST只是源代码的另一种表示形式。 这些中间表示链接在一起代表抽象层。

这些层的层级没有限制,每一个新层都让源代码的表示形式更接近机器码。

优化层

但是,不是所有层都仅用于翻译。 许多编译器还另外尝试优化人工编写的代码。 (通常在编写代码时要兼顾代码优雅和代码性能)。

我们来看一个JavaScripte代码的例子:

for (var i = 0, acc = 0; i < arr.length; i++)acc += arr[i];

如果编译器直接从AST(抽象语法树)翻译到机器码,大致像下面的过程这样(抽象表达,并非真实指令集):

acc = 0;
i = 0;
loop {// Load `.length` field of arrtmp = loadArrayLength(arr);if (i >= tmp)break;// Check that `i` is between 0 and `arr.length`// (NOTE: This is necessary for fast loads and// stores).checkIndex(arr, i);// Load valueacc += load(arr, i);// Increment indexi += 1;
}

可能不是那么显而易见,但是此代码远非最优。 数组的长度实际上不会在循环内部更改,并且根本不需要范围检查。 理想情况下,它应该如下所示:

acc = 0;
i = 0;
len = loadArrayLength(arr);
loop {if (i >= tmp)break;acc += load(arr, i);i += 1;
}

让我们尝试想象一下如何做到这一点。

假设我们手头有一个AST,我们尝试直接从它翻译生成机器码:

(注意:下面的抽象语法树是使用 esprima工具生成)

{ type: 'ForStatement',//// This is `var i = 0;`//init:{ type: 'VariableDeclaration',declarations:[ { type: 'VariableDeclarator',id: { type: 'Identifier', name: 'i' },init: { type: 'Literal', value: 0, raw: '0' } },{ type: 'VariableDeclarator',id: { type: 'Identifier', name: 'acc' },init: { type: 'Literal', value: 0, raw: '0' } }],kind: 'var' },//// `i < arr.length`//test:{ type: 'BinaryExpression',operator: '<',left: { type: 'Identifier', name: 'i' },right:{ type: 'MemberExpression',computed: false,object: { type: 'Identifier', name: 'arr' },property: { type: 'Identifier', name: 'length' } } },//// `i++`//update:{ type: 'UpdateExpression',operator: '++',argument: { type: 'Identifier', name: 'i' },prefix: false },//// `arr[i] += 1;`//body:{ type: 'ExpressionStatement',expression:{ type: 'AssignmentExpression',operator: '+=',left: { type: 'Identifier', name: 'acc' },right:{ type: 'MemberExpression',computed: true,object: { type: 'Identifier', name: 'arr' },property: { type: 'Identifier', name: 'i' } } } }

上面的JSON数据也可以可视化为下图:

这是一棵树,因此从顶部到底部遍历访问它很自然,当我们访问AST节点时就生成对应的机器码。 这种方法的问题在于,有关变量的信息非常稀疏,并且分布在不同的树节点上。

同样,为了安全地将长度查找移出循环,我们需要知道数组长度在循环的迭代之间不会改变。 人们只要看一下源代码就可以轻松地做到这一点,但是编译器需要做大量工作才能从AST中提取到这些信息。

像许多其他编译器问题一样,通常可以通过将数据提升到更合适的抽象层(即中间表示)中来解决此问题。 在这个特例里,IR的选择称为数据流图(DFG)。 与其关注语法实体(例如用于循环,表达式等),不如关注数据本身(读取,变量值)以及它们如何在程序中变化。

数据流图(DFG,Data-flow Graph)

在这个例子中,我们感兴趣的数据是变量arr的值。 我们希望能够轻松观察它的所有用法,以验证没有越界访问,也没有任何其他会改变数组长度的修改。

这是通过在不同数据值之间引入“def-use”(定义和使用)关系来实现的。 具体而言,这意味着该值已被声明过一次(节点),并且已在某处用于创建新值(每条边代表一次使用)。 显然,将不同的值连接在一起将形成一个数据流图,如下所示:

译者注:图中实线箭头表示该值的用途,虚线表示控制依赖项。

注意这张大图中的红色图框,实线箭头表示该值的用途。 通过这些实线遍历各节点,编译器可以得出在以下位置使用了array的值:

  • loadArrayLength
  • checkIndex
  • load

如果以破坏性方式(即保存长度大小)访问array节点的值,明确地“克隆”array节点来构造此图形。每当遇到array节点,观察它的用法,我们总是可以确定它的值不会改变。

听起来可能很复杂,但是图形的这个属性非常容易实现。 该图应遵循Single Static Assignment(SSA,单一静态赋值)规则。 简而言之,要将任何程序转换为SSA,编译器需要为所有赋值操作的变量以及其后续的使用改名,以确保每个变量仅赋值一次。

来看个例子,应用SSA之前:

var a = 1;
console.log(a);
a = 2;
console.log(a);

应用SSA之后:

var a0 = 1;
console.log(a0);
var a1 = 2;
console.log(a1);

这样,我们可以确定当谈论a0时--实际上是在谈论它的单个赋值。 这与人们在函数式语言中的工作方式非常接近!

由于loadArrayLeng没有控制依赖项(即没有虚线;我们将在稍后讨论它们),编译器可能会得出结论:该节点可以自由地移动到它想要的任何位置,并且可以放置在循环之外。通过进一步查看这个图,我们可以观察到ssa:phi节点的值始终在0和arr.length之间,因此可以将checkIndex一起删除。

很整洁,不是吗?

控制流图(CFG, Control Flow Graph)

我们使用了某种形式的数据流分析(data-flow analysis ),来从程序中提取信息。 这使我们可以对如何优化进行安全的假设。

这种数据流表示形式在许多其他情况下非常有用。 唯一的问题是,通过将我们的代码转换成这种图形,我们在表示链(从源代码到机器代码)中倒退了一步。 这种中间表示甚至比AST更不适合生成机器码。

原因在于机器指令是顺序命令列表,CPU依次执行这些指令。 我们得到的图形无法表达这一点。 实际上,它根本没有排序。

通常,这可以通过将图节点分组为块来解决这个问题。 这种表示形式称为控制流程图(CFG)。 例如:

b0 {i0 = literal 0i1 = literal 0i3 = arrayi4 = jump ^b0
}
b0 -> b1b1 {i5 = ssa:phi ^b1 i0, i12i6 = ssa:phi ^i5, i1, i14i7 = loadArrayLength i3i8 = cmp "<", i6, i7i9 = if ^i6, i8
}
b1 -> b2, b3
b2 {i10 = checkIndex ^b2, i3, i6i11 = load ^i10, i3, i6i12 = add i5, i11i13 = literal 1i14 = add i6, i13i15 = jump ^b2
}
b2 -> b1b3 {i16 = exit ^b3
}

它被称为图不是没有原因的。 例如,bXX块表示节点,bXX-> bYY箭头表示边。 让我们对其进行可视化:

如您所见,循环之前的代码在块b0,循环头在b1,循环测试在b2,循环主体在b3,退出节点在b4。

从这种形式转换为机器码就非常容易了。 我们只需将iXX标识符替换为CPU寄存器名称(从某种意义上讲,CPU寄存器是某种变量,CPU的寄存器数量有限,因此我们需要注意不要耗尽它们),并且一行接一行地为每条指令生成机器码。

回顾一下,CFG具有数据流关系并且有序。 这使我们能够将其用于数据流分析和机器代码生成。 但是,通过操纵块及其包含的内容来优化CFG可能会很快变得复杂且容易出错。

Clifford Click和Keith D. Cooper建议使用一种称为sea-of-nodes的方法来改善,这是本文的主题!

Sea-of-Nodes

还记得前面数据流图中的虚线吗? 正是这些虚线能让数据流图成为Sea-of-Nodes图。

我们选择将控件依赖项声明为图中的虚线边,而不是将节点分组放到块里并对节点进行排序。 如果我们拿到这个数据流图,删除所有未用虚线连接的节点,然后对它们进行分组,我们将得到下图:

通过一点想象和对节点重新排序,我们就可以看到此图与简化的CFG图相同:

让我们再看一下sea-of-nodes表示形式:

该图与CFG的显著区别在于,除了具有控制依赖性的节点(换言之,参与控制流的节点)之外,其他的节点没有排序。

这种表示形式是查看代码非常有效的方法。 它具有一般数据流图的所有内容,并且可以轻松更改,而无需不断删除/替换块中的节点。

简化(reductions)

说到更改,我们来讨论下修改图形的方法。 Sea-of-nodes图通常通过对图进行简化来修改。 我们将图中的所有节点排队,然后为队列中的每个节点调用简化函数。 简化函数涉及的所有操作(更改,替换)都将排入队列,稍后传递给该函数。 如果你有很多简化操作,则可以把它们堆叠在一起并在队列中的每个节点上调用它们,如果它们依赖于彼此的最终状态,则可以逐个应用它们。事情简单的就像念一个符咒!

我为sea-of-nodes实践编写了一个JavaScript工具集,其中包括:

  • json-pipeline -图的生成器和标准库。 提供创建节点,向节点添加输入,更改其控制依赖性以及向/从可打印数据导出/导入图的方法!
  • json-pipeline-reducer – 简化(reductions)引擎。 只需创建一个reducer实例,为它提供几个reduce函数,然后在现有的json-pipeline图上执行这个reducer。
  • json-pipeline-scheduler – 这是一个库,用于将无序图放回由控制边(虚线)连接在一起的有限数量的块中

这些工具结合在一起,可以解决许多用数据流方式表示的问题。

下面有一个简化(reductions)的示例,它将优化这段JS代码:

for (var i = 0, acc = 0; i < arr.length; i++)acc += arr[i];

简化(reductions)相关的代码块很大,如果你想跳过它,可以只看下面这些简介:

  • 计算各个节点的整数范围:literal, add, phi
  • 计算适用于分支部分的限制
  • 应用范围(range)和限制(limit)信息 (i 始终是受arr.length限制的非负数 ) 而得出结论,长度检查是不必要的,可以删除
  • json-pipeline-scheduler会自动将arr.length移出循环,这是因为它执行全局代码移动(Global Code Motion )来调度块中的节点。
// Just for viewing graphviz output
var fs = require('fs');var Pipeline = require('json-pipeline');
var Reducer = require('json-pipeline-reducer');
var Scheduler = require('json-pipeline-scheduler');//
// Create empty graph with CFG convenience
// methods.
//
var p = Pipeline.create('cfg');//
// Parse the printable data and generate
// the graph.
//
p.parse(`pipeline {b0 {i0 = literal 0i1 = literal 0i3 = arrayi4 = jump ^b0}b0 -> b1b1 {i5 = ssa:phi ^b1 i0, i12i6 = ssa:phi ^i5, i1, i14i7 = loadArrayLength i3i8 = cmp "<", i6, i7i9 = if ^i6, i8}b1 -> b2, b3b2 {i10 = checkIndex ^b2, i3, i6i11 = load ^i10, i3, i6i12 = add i5, i11i13 = literal 1i14 = add i6, i13i15 = jump ^b2}b2 -> b1b3 {i16 = exit ^b3}
}`, { cfg: true }, 'printable');if (process.env.DEBUG)fs.writeFileSync('before.gv', p.render('graphviz'));//
// Just a helper to run reductions
//function reduce(graph, reduction) {var reducer = new Reducer();reducer.addReduction(reduction);reducer.reduce(graph);}//
// Create reduction
//
var ranges = new Map();function getRange(node) {if (ranges.has(node))return ranges.get(node);var range = { from: -Infinity, to: +Infinity, type: 'any' };ranges.set(node, range);return range;
}function updateRange(node, reducer, from, to) {var range = getRange(node);// Lowest type, can't get upwardsif (range.type === 'none')return;if (range.from === from && range.to === to && range.type === 'int')return;range.from = from;range.to = to;range.type = 'int';reducer.change(node);
}function updateType(node, reducer, type) {var range = getRange(node);if (range.type === type)return;range.type = type;reducer.change(node);
}//
// Set type of literal
//
function reduceLiteral(node, reducer) {var value = node.literals[0];updateRange(node, reducer, value, value);
}function reduceBinary(node, left, right, reducer) {if (left.type === 'none' || right.type === 'none') {updateType(node, reducer, 'none');return false;}if (left.type === 'int' || right.type === 'int')updateType(node, reducer, 'int');if (left.type !== 'int' || right.type !== 'int')return false;return true;
}//
// Just join the ranges of inputs
//
function reducePhi(node, reducer) {var left = getRange(node.inputs[0]);var right = getRange(node.inputs[1]);if (!reduceBinary(node, left, right, reducer))return;if (node.inputs[1].opcode !== 'add' || left.from !== left.to)return;var from = Math.min(left.from, right.from);var to = Math.max(left.to, right.to);updateRange(node, reducer, from, to);
}//
// Detect: phi = phi + <positive number>, where initial phi is number,
// report proper range.
//
function reduceAdd(node, reducer) {var left = getRange(node.inputs[0]);var right = getRange(node.inputs[1]);if (!reduceBinary(node, left, right, reducer))return;var phi = node.inputs[0];if (phi.opcode !== 'ssa:phi' || right.from !== right.to)return;var number = right.from;if (number <= 0 || phi.inputs[1] !== node)return;var initial = getRange(phi.inputs[0]);if (initial.type !== 'int')return;updateRange(node, reducer, initial.from, +Infinity);
}var limits = new Map();function getLimit(node) {if (limits.has(node))return limits.get(node);var map = new Map();limits.set(node, map);return map;
}function updateLimit(holder, node, reducer, type, value) {var map = getLimit(holder);if (!map.has(node))map.set(node, { type: 'any', value: null });var limit = map.get(node);if (limit.type === type && limit.value === value)return;limit.type = type;limit.value = value;reducer.change(holder);
}function mergeLimit(node, reducer, other) {var map = getLimit(node);var otherMap = getLimit(other);otherMap.forEach(function(limit, key) {updateLimit(node, key, reducer, limit.type, limit.value);});
}//
// Propagate limit from: X < Y to `if`'s true branch
//
function reduceIf(node, reducer) {var test = node.inputs[0];if (test.opcode !== 'cmp' || test.literals[0] !== '<')return;var left = test.inputs[0];var right = test.inputs[1];updateLimit(node.controlUses[0], left, reducer, '<', right);updateLimit(node.controlUses[2], left, reducer, '>=', right);
}//
// Determine ranges and limits of
// the values.
//var rangeAndLimit = new Reducer.Reduction({reduce: function(node, reducer) {if (node.opcode === 'literal')reduceLiteral(node, reducer);else if (node.opcode === 'ssa:phi')reducePhi(node, reducer);else if (node.opcode === 'add')reduceAdd(node, reducer);else if (node.opcode === 'if')reduceIf(node, reducer);}
});
reduce(p, rangeAndLimit);//
// Now that we have ranges and limits,
// time to remove the useless array
// length checks.
//function reduceCheckIndex(node, reducer) {// Walk up the control chainvar region = node.control[0];while (region.opcode !== 'region' && region.opcode !== 'start')region = region.control[0];var array = node.inputs[0];var index = node.inputs[1];var limit = getLimit(region).get(index);if (!limit)return;var range = getRange(index);// Negative array index is not validif (range.from < 0)return;// Index should be limited by array lengthif (limit.type !== '<' ||limit.value.opcode !== 'loadArrayLength' ||limit.value.inputs[0] !== array) {return;}// Check is safe to remove!reducer.remove(node);
}var eliminateChecks = new Reducer.Reduction({reduce: function(node, reducer) {if (node.opcode === 'checkIndex')reduceCheckIndex(node, reducer);}
});
reduce(p, eliminateChecks);//
// Run scheduler to put everything
// back to the CFG
//var out = Scheduler.create(p).run();
out.reindex();if (process.env.DEBUG)fs.writeFileSync('after.gv', out.render('graphviz'));console.log(out.render({ cfg: true }, 'printable'));

感谢阅读此文。 敬请期待有关这种sea-of-nodes方法的更多信息。

特别感谢 Paul Fryzel对此文进行了校对,并提供了宝贵的反馈和语法修改!

译注:如果想测试上面的js代码,需要安装node、mocha,用mocha 运行上面的js。

如果打开debug模式,会输出优化前后的有向图文件,可以用GraphViz工具打开。

附录:附加库 assert-text

Sea of nodes 中译文相关推荐

  1. 为什么要阅读——兼分享《首先,打破一切常规》[中译文]:世界顶级管理者的成功秘诀/(美)马库斯·白金汉,(美)柯特·科夫曼 著...

    <首先,打破一切常规>[中译文]:世界顶级管理者的成功秘诀/(美)马库斯·白金汉,(美)柯特·科夫曼 著.鲍世修 等译 下载地址:http://pan.baidu.com/s/1mgkca ...

  2. 为什么要阅读——兼分享《首先,打破一切常规》[中译文]:世界顶级管理者的成功秘诀/(美)马库斯#183;白金汉,(美)柯特#183;科夫曼 著...

    <首先,打破一切常规>[中译文]:世界顶级管理者的成功秘诀/(美)马库斯·白金汉,(美)柯特·科夫曼 著:鲍世修 等译 下载地址:http://pan.baidu.com/s/1mgkca ...

  3. 为什么要阅读——兼分享《首先,打破一切常规》[中译文]:世界顶级管理者的成功秘诀/(美)马库斯·白金汉,(美)柯特·科夫曼 著

    <首先,打破一切常规>[中译文]:世界顶级管理者的成功秘诀/(美)马库斯·白金汉,(美)柯特·科夫曼 著:鲍世修 等译 下载地址:http://pan.baidu.com/s/1mgkca ...

  4. 【转载】Amit’s A star Page 中译文

    GameRes游戏开发资源网 http://www.gameres.com   Amit's A star Page中译文  Amit's A star Page中译文原文链接:http://dev. ...

  5. [转]Amit's Astar Page中译文

    如此好贴,不能不转!原文地址:http://dev.gameres.com/Program/Abstract/Arithmetic/AmitAStar.mht 本文版权归原作者.译者所有,我只是转贴: ...

  6. MatLab中画树状图方法treeplot(nodes)中描述树结构的矢量nodes的构造

    按要求需要用matlab画树状图,在网络上找到两句代码: nodes = [0 1 2 2 4 4 4 1 8 8 10 10]; treeplot(nodes); 画出了如下图: 其中矢量nodes ...

  7. Hadoop datanode正常启动,但是jps差不多datanode进程,而且Live nodes中却缺少节点

    启动时可以看到启动成功,但是在chun2,jps的时候却没有了datanode进程,而且web端Live nodes也缺少了 百度搜索之后查到是因为hdfs.site.xml配置文件里dfs.data ...

  8. 海龟交易法则(中译文)

    前言 查看全文 http://www.taodudu.cc/news/show-2863772.html 相关文章: 数学笔记28--不定式和洛必达法则 Excel学习笔记:P33-来自2/8法则的神 ...

  9. Intel(R) Software Guard Extensions Developer Guide 参考中译文,阅读

    阅读: https://github.com/dingelish/SGXfail/blob/master/06.md 简介 这个运行时系统(run-time System)包括如下: Untruste ...

最新文章

  1. 极小连通子图和极大连通子图_强连通分量与拓扑排序
  2. Linux 下ntpdate网络校时使用
  3. python入门基础代码图-Python入门基础学习一
  4. 开始新的blog之旅--flash3,0涂鸦板保存,撤销功能
  5. linux 树型显示文件 tree ls tree 命令
  6. 一文学会如何使用Java的交互式编程环境 JShell
  7. Win10 64位系统运行汇编程序(使用masm与dosbox)
  8. 支付宝即时到账在线语音音效生成器html源码
  9. 计算机工作记录,电脑上可以记录每日工作内容的办公便签是什么?
  10. mysql的备份与还原步骤_MySQL备份与还原
  11. C++_深浅拷贝详解
  12. 论人类不平等起源读后感
  13. Java8新特性 Stream流常用方法
  14. Excel批量合并相同内容单元格操作——WPS太秀了
  15. shell中vi的基本操作及Xshell 常用命令
  16. 利用Fiddler抓包解析,轻松下载m3u8格式网络视频
  17. Android开发5年,面试问到底层实现原理,被怼得,程序员中年危机
  18. Lua热更原理以及加载规则
  19. nginx变量传递给php,php-从nginx将参数传递给auth_request模块
  20. Value Use User

热门文章

  1. Unity时光倒流效果实现
  2. 华硕FL5900U如何关闭ahci_实战华硕B360主板RX580显卡安装苹果macOS 10.14 Mojave
  3. 风云邀请成为IT168社区Silverlight版主
  4. 元启发式如何跳出局部最优?
  5. linux nginx rpm 安装配置,Centos下安装nginx rpm包
  6. google search
  7. ChatGPT中文网
  8. windows server2012安装web服务以及运行asp
  9. IPD解读—需求管理(OR)流程方法论
  10. 用C语言学生成绩数据库排序功能设计,[c语言学生成绩管理系统]C语言学生成绩管理系统实验报告...