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

qinfanpeng · 发布于 2015年11月13日 · 最后由 kevinyu 回复于 2016年10月09日 · 7579 次阅读
3790
本帖已被设为精华帖!

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时,他喜欢制作传统手工家具、阅读历史、在位于西雅图的车库中酿造啤酒。

共收到 20 条回复
8658

学到 。 已赞

1638

:plus1:

17441

很棒 :plus1:

20099

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

2564

:thumbsup:

20099

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

11289

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

2419

👏🏻

3790

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

3673

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

3790

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

9500

谢谢,赞

14352

学到了 赞

15710

:plus1:

24114

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

3790

好高兴能帮到你 😄

24262
Good
19671

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

20859 adamshen Ruby 程序怎么执行的 中提及了此贴 11月08日 13:01
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册