Sea of nodes 中译文
原文链接:https://darksi.de/d.sea-of-nodes/
简介
这篇文章将讲述我最近学到的Sea of nodes编译器概念。
尽管不是完全必要,但在阅读本文之前,可以先看一下我以前在JIT编译器上发表的一些文章,应该会很有一些帮助:
编译器=翻译器
编译器是软件工程师每天都要使用的工具。令人惊讶的是,即使是那些认为自己不会编写代码的人,在一天当中仍然会大量使用编译器。这是因为大多数Web页都依赖于客户端代码的执行,并且很多这样的客户端程序都以源代码的形式传递给浏览器,例如javascript。
在这里,我们要讨论一件重要的事:尽管源代码(通常)是人类可读的,但对于您的笔记本电脑/计算机/手机/ ...的CPU来说,它几乎是垃圾。 另一方面,计算机可以读取的机器代码几乎总是人类难以阅读的。 我们必须要做一些事情来处理它,此问题的解决方案称为翻译的过程。
很少有编译器只执行一次翻译:就从源代码直接到机器代码。 在实践中,大多数编译器至少要经过两次翻译过程:从源代码到抽象语法树(AST),从AST到机器码。 在这种情况下,AST的作用类似于中间表示(IR),顾名思义,AST只是源代码的另一种表示形式。 这些中间表示链接在一起代表抽象层。
这些层的层级没有限制,每一个新层都让源代码的表示形式更接近机器码。
优化层
但是,不是所有层都仅用于翻译。 许多编译器还另外尝试优化人工编写的代码。 (通常在编写代码时要兼顾代码优雅和代码性能)。
我们来看一个JavaScripte代码的例子:
[code]for (var i = 0, acc = 0; i < arr.length; i++) acc += arr[i];
如果编译器直接从AST(抽象语法树)翻译到机器码,大致像下面的过程这样(抽象表达,并非真实指令集):
[code]acc = 0; i = 0; loop { // Load `.length` field of arr tmp = 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 value acc += load(arr, i); // Increment index i += 1; }
可能不是那么显而易见,但是此代码远非最优。 数组的长度实际上不会在循环内部更改,并且根本不需要范围检查。 理想情况下,它应该如下所示:
[code]acc = 0; i = 0; len = loadArrayLength(arr); loop { if (i >= tmp) break; acc += load(arr, i); i += 1; }
让我们尝试想象一下如何做到这一点。
假设我们手头有一个AST,我们尝试直接从它翻译生成机器码:
(注意:下面的抽象语法树是使用 esprima工具生成)
[code]{ 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之前:
[code]var a = 1; console.log(a); a = 2; console.log(a);
应用SSA之后:
[code]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)。 例如:
[code]b0 { i0 = literal 0 i1 = literal 0 i3 = array i4 = jump ^b0 } b0 -> b1 b1 { i5 = ssa:phi ^b1 i0, i12 i6 = ssa:phi ^i5, i1, i14 i7 = loadArrayLength i3 i8 = cmp "<", i6, i7 i9 = if ^i6, i8 } b1 -> b2, b3 b2 { i10 = checkIndex ^b2, i3, i6 i11 = load ^i10, i3, i6 i12 = add i5, i11 i13 = literal 1 i14 = add i6, i13 i15 = jump ^b2 } b2 -> b1 b3 { 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代码:
[code]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 )来调度块中的节点。
[code]// 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 0 i1 = literal 0 i3 = array i4 = jump ^b0 } b0 -> b1 b1 { i5 = ssa:phi ^b1 i0, i12 i6 = ssa:phi ^i5, i1, i14 i7 = loadArrayLength i3 i8 = cmp "<", i6, i7 i9 = if ^i6, i8 } b1 -> b2, b3 b2 { i10 = checkIndex ^b2, i3, i6 i11 = load ^i10, i3, i6 i12 = add i5, i11 i13 = literal 1 i14 = add i6, i13 i15 = jump ^b2 } b2 -> b1 b3 { 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 upwards if (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 chain var 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 valid if (range.from < 0) return; // Index should be limited by array length if (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
- 点赞
- 收藏
- 分享
- 文章举报
- 需要中文版《The Scheme Programming Language》的朋友可以在此留言(内附一小段译文)
- 24. Swap Nodes in Pairs
- Storyboard介绍及使用2 Storyboards Part2 译文
- 24. Swap Nodes in Pairs
- leetcode-24 Swap Nodes in Pairs
- 移除行块级元素之间的空格(译文)
- lintcode-medium-Route Between Two Nodes in Graph
- 如何使用Linux Epoll来进行网络程序开发(译文)
- 《Deep Learning》译文 第一章 前言(中) 神经网络的变迁与称谓的更迭
- elasticsearch报错:None of the configured nodes are available: []
- 理解Java类加载机制(译文)
- LintCode:Swap Two Nodes in Linked List
- leetcode-24-Swap Nodes in Pairs
- <LeetCode OJ> 24. Swap Nodes in Pairs
- LeetCode第25题之Reverse Nodes in k-Group
- 【leetcode】24. Swap Nodes in Pairs
- Hadoop datanode正常启动,但是Live nodes中却缺少节点的问题
- LeetCode代码记录 24 Swap Nodes in Pairs
- Erlang调度器的一些细节以及它重要的原因(译文)
- elasticsearch报错解决办法:NoNodeAvailableException[None of the configured nodes are available