翻译 Ruby 是如何解释运行程序的

qinfanpeng · 2015年11月13日 · 最后由 kevinyu 回复于 2016年10月09日 · 14546 次阅读
本帖已被管理员设置为精华贴

Ruby 是如何解释运行程序的

作 者:Starr Horne 原文地址:http://blog.honeybadger.io/how-ruby-interprets-and-runs-your-programs/
前段时间在帮一位前辈校对《Ruby 原理剖析》,而这篇文章正好相关,就试着翻译一下。这是我第一次在这献丑,请大家多提建设性意见。

作为开发人员,对手中的工具越了解,就越利于做出准确的判断。这很有用,尤其是定位性能问题的时候,了解 Ruby 运行底层的运行活动就格外重要。

这篇文章将带你体验一段小程序的分词、解析、编译之旅。我们将用 Ruby 提供的工具来窥探解释器的每一步活动。

别担心,即使你不是专家,这篇文章对你来说也很易懂。这并非高深的技术手册,而只是一篇普通指南。

初遇小程序

我会用 if/else 语句作为示例。为了节约空间,这里用三元操作符写的,别较真啦,这其实就是 if/else。

  100 ? 'foo' : 'bar'

可别小看这段程序,随着 Ruby 解释处理,信息量很大的。

:本文的示例是基于 Ruby(MRI)2.2 的,如果你用的是其他实现(或其他历史版本),结果可能不同。

分词(Tokenizing)

在运行程序之前,Ruby 解释器得先将形式自由的程序语句转换成加更结构化的数据。

第一步便是将程序切分成块(chunks),被称之为词条 (token)

# 程序字符串
"x > 1"

# 词条(tokens)
["x", ">", "1"]

Ruby 标准库提供的Ripper模块可以帮我们模拟 Ruby 解释器处理代码的过程。

下面示例在代码上调用了tokenize,如你所见,返回了词条数组。

require 'ripper'
Ripper.tokenize("x > 1 ? 'foo' : 'bar'")
# => ["x", " ", ">", " ", "1", " ", "?", " ", "'", "foo", "'", " ", ":", " ", "'", "bar", "'"]

分词器 (tokenizer) 有点傻,即使给的是非法代码,它还是会很 happy 地分词。

# bad code
Ripper.tokenize("1var @= \/foobar`")
# => ["1", "var"]

词法解析 (Lexing)

紧随分词之后的是词法解析。虽然还是词条,但之上多了附加信息。

下面的示例用Ripper对程序进行词法解析。如你所示,这次给每个词条打上了标签,如代表 identifier 的:on_ident、代表 operator 的:on_op、代码 Integer 的:on_int等。

require 'ripper'
require 'pp'

pp Ripper.lex("x > 100 ? 'foo' : 'bar'")

# [[[1, 0], :on_ident, "x"],
#  [[1, 1], :on_sp, " "],
#  [[1, 2], :on_op, ">"],
#  [[1, 3], :on_sp, " "],
#  [[1, 4], :on_int, "100"],
#  [[1, 5], :on_sp, " "],
#  [[1, 6], :on_op, "?"],
#  [[1, 7], :on_sp, " "],
#  [[1, 8], :on_tstring_beg, "'"],
#  [[1, 9], :on_tstring_content, "foo"],
#  [[1, 12], :on_tstring_end, "'"],
#  [[1, 13], :on_sp, " "],
#  [[1, 14], :on_op, ":"],
#  [[1, 15], :on_sp, " "],
#  [[1, 16], :on_tstring_beg, "'"],
#  [[1, 17], :on_tstring_content, "bar"],
#  [[1, 20], :on_tstring_end, "'"]]

目前为止,还是没有实质性的语法检测,词法解析器 (lexer) 还是会傻傻地处理非法代码。

语法解析 (Parsing)

现在代码已被拆分了成更可操作的词条,是时候进行词法解析了。

词法解析阶段,Ruby 会将词条文本转换成“抽象语法树”(abstract syntax tree),简称 AST。“抽象语法树”是程序在内存中的表现形式。

你可能会说,整体来看编程语言其实就是更友好的抽象语法树而已。

require 'ripper'
require 'pp'

pp Ripper.sexp("x > 100 ? 'foo' : 'bar'")

# [:program,
#  [[:ifop,
#    [:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
#    [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
#    [:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]

这些输出可能不那么容易理解。不过细看之下,相信你能看出与源码的对应关系的。

# 定义程序
[:program,
# if比较
[[:ifop,
# 条件检测 (x > 100)
[:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
# 若为true,则返回“foo”
[:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
# 若为true,则返回“foobar”
[:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]

现在 Ruby 解释器知道你具体的目的了,可以马上运行程序了。在 Ruby 1.9 及以前的版本中,确实如此,但现在(Ruby1.9 以后)还有最后一步要做。

编译成字节码 (Compiling to bytecode)

不再是直接遍历抽象语法树,如今的 Ruby 会将抽象语法树编译成更底层的字节码。随后这些字节码会在 Ruby 虚拟机(Ruby virtual machine)中运行。

我们可以通过RubyVM::InstructionSequence来窥探看一下 Ruby 虚拟机内部的工作。下面的示例先编译了代码,然后再反编译成更可读的格式。

puts RubyVM::InstructionSequence.compile("x > 100 ? 'foo' : 'bar'").disassemble
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putself
# 0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 putobject        100
# 0007 opt_gt           <callinfo!mid:>, argc:1, ARGS_SIMPLE>
# 0009 branchunless     15
# 0011 putstring        "foo"
# 0013 leave
# 0014 pop
# 0015 putstring        "bar"
# 0017 leave

哇塞!突然看起来更像汇编而非 Ruby 了。下面来逐一浏览一遍,看看能否理解它们。

# 调用self的`x`方法,并将结果存入栈中。
0002 putself
0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>

# 将100入栈
0005 putobject        100

# 进行 (x > 100)比较
0007 opt_gt           <callinfo!mid:>, argc:1, ARGS_SIMPLE>

# 若比较结果为false,则跳转至15行
0009 branchunless     15

# 若为true,则返回“foo”
0011 putstring        "foo"
0013 leave
0014 pop

# 这就是15行,如果比较为false,便跳转至此,然后返回“bar”
0015 putstring        "bar"
0017 leave

然后 Ruby 虚拟机(YARV)便会遍历并执行这些指令。就这样!

小结

至此,这个简单好玩的 Ruby 运行之旅便结束了。利用本文提到的这些工具,可以验证很多关于 Ruby 解释程序的猜想。相信我,再也没有比抽象语法树 AST 更“不抽象”了(这是句玩笑话)。下次你又被某个奇葩的性能问题所困时,不妨看看对应的字节码。这可能不会直接解决你的问题,但却可能给你灵感。

关于作者 Starr Horne: Starr Horne,Rubyist,Honeybadger.io 的 Javascript 主程。没沉迷于解决他人的 bug 时,他喜欢制作传统手工家具、阅读历史、在位于西雅图的车库中酿造啤酒。

学到。已赞

很棒 :plus1:

ruby 的类运行以后是象 java 一样常驻内存的吗?

那 java 和 ruby 的执行过程是一样的,只不过 java 是要手动 javac 成字节码 ruby 这个过程是自动的

不错,记得 http://book.douban.com/subject/24718740/ 这本书也有详细的文章和实验部分。

#8 楼 你说的那本书,对应中文版叫做《Ruby 原理剖析》,也在路上了。

虽然看过英文版但是还是想收藏一本中文的,上次私信汉东说要年后才出版~~只能等了

貌似是年后出吧,他还在打磨。

谢谢,赞

学到了 赞

帮助很大,如果初学遇到不懂或者不好理解的语法可以这样分析了。谢谢 👏

好高兴能帮到你 😄

非常棒的小例子,有头有尾看起来很易懂,赞赞

adamshen Ruby 程序怎么执行的 提及了此话题。 11月08日 13:01
qinfanpeng 判断 Rails partial 中变量是否定义的正确姿势 提及了此话题。 03月20日 06:21
需要 登录 后方可回复, 如果你还没有账号请 注册新账号