InfoQ

新闻

Rubinius内部细节:线程、对象空间和调试

作者 Werner Schuster译者 高昂 发布于 2007年8月6日 上午3时0分

社区
Ruby
主题
调试,
性能和可伸缩性,
动态语言
标签
语言特性,
SmallTalk,
多线程,
并发,
Rubinius

作为Evan Phoenix谈Rubinius:虚拟机内幕面面观的续篇,本文的第二部分将更加深入细节的实现。

Ruby 1.8.x目前使用用户空间的线程,意味着它无法充分利用多核技术带来的便利,因为操作系统只能感知确定一个线程。Rubinius目前使用用户空间线程,但是Evan同时也考虑了其它的解决方案:

对于Rubinius来说,在相同的地址空间中实现多重解释器是件价值不高的事情。编写整个虚拟机的时候就注意到了本地线程安全和可重入机制。这一切来自我在Sydney项目的工作经验,这个项目是对Ruby 1.8.2版的整理。

你可以轻松地创建两个Machine(基础的数据结构)并同时将其初始化。它们随后都会保持完全的独立性。唯一要做的工作就是确保两个Machine可以合理地调度它们的代码。你甚至可以在两个不同的本地线程中启动两个虚拟机实例并且使其通过通道进行相互通信,这将给你带来一个真正可以利用多处理器特性的Ruby。

在标准的Ruby发布版中,包含简单的调试器,它使用跟踪特性来实现。我们可以通过设置回调函数的方法来使用这个调试器,而这个回调函数在每行新的代码被执行之前被调用。回调函数通过set_trace_func方法来进行注册。(这种方式同样也用于概要分析)。这种方法带来的问题是它较高的系统开销。目前Ruby代码每执行一行,意味着要调用跟踪功能并且需要确定是否在这一点上延迟执行。另外也存在其它使用本体扩展的解决方案,就像ruby-debug或者Visual Studio 2005的Ruby in Steel插件所包含的Cylon调试器一样。

类似于Ruby.NET、IronRuby或是XRuby的Ruby编译器,恰好把调试信息生成在目标IL或字节码中,并且使用各自虚拟机的调试特性。

Rubinius最近也获得了代码调试的支持,实现了断点的功能,它只有在某个断点命中的时候才会引入额外的性能开销。Evan对于功能实现这样解释道:

我已经实现了基本的调试设施,这为我们带来了全速的断点(Full Speed Breakpoints)。全速断点意味着在运行期我们不会因为使用调试器而带来任何性能上的损耗了。我认为这是一次巨大的胜利,因为我听说Ruby调试器的速度一直以来是个瓶颈。我已经开始在FSB的基础之上建立更高级层次的功能,最终,我将可能将其写入类似于ruby-debug的机制当中,或是至少在感觉上是类似于ruby-debug的机制之中。

从技术角度说,FSB通过字节码替换进行工作。当断点设置之后,系统使用反射机制查找精确的CompiledMethod对象,在这里需要设置调试断点。随后,系统计算字节码,判断断点需要在何处发生,并且使用叫做yield_debugger的奇妙的方式替代目前的指令。当命中这条指令时,系统将控制权交给调试器,这个调试器附属于当前运行的线程上。当这个方法需要继续执行时,旧的指令又被交换回来并且指令指针的值回退1,并被重新激活。

这个功能运行得很好,因为调试器只是设置为空闲状态,等待运行实际代码的线程与之建立联系。这个功能运行得很好的另外一个原因是Rubinius中的方法上下文(Method Contexts)是被作为一等公民对待的。方法上下文和栈框架(Stack Frame)其实是一回事,它描述了方法运行的状态。在Rubinius中,你可以从虚拟机中获取系统中任何一个状态的方法上下文。随后就可以通过检查那个对象来发现此刻具体发生的事情。得到方法上下文最简单的方法就是调用“MethodContext.current”,将返回当前运行方法的方法上下文。

随着例如JVM和CLR这样Ruby托管运行时方式的实现,Ruby的ObjectSpace特性已经出现了一点问题。ObjectSpace允许对Ruby堆中所有可触及的对象进行访问,比方说:

ObjectSpace::each_object(Class){|x|
p x
}

上述代码将打印当前Ruby软件栈中的所有Class对象。

JRuby的开发者Ola Bini最近撰写了关于ObjectSpace在JRuby环境下的性能影响一文。因为JVM(或者是CLR)不允许直接访问堆,必须跟踪每一个对象的创建并且保留所有活动对象的清单。Evan对于rubinius中ObjectSpace的情况,这样解释道:

我们实际上目前还没有实现它。我们将不会像JRuby实现那样有这么多的麻烦,因为我们具有对象存储单元的直接访问能力。

Smalltalk语言实现这样的行为主要使用被称作next_object的单一primitive实现。当你在大多数对象中调用next_object时,将返回紧随它们之后的一个对象。目前,之后的定义是与具体实现相关的,但是通常的定义是在内存中当前对象的下一个对象。

需要注意到这种方式在任何情况下都不可能保证是无损或者精确的,并且,为了以一种不困扰使用这套接口的开发人员的方式完成,这种方式仍然会有一些性能上的额外开支。这可能令人困惑,因为虚拟机(包括Rubinius)依赖于这样一个事实:它们可以在内存中重新整理对象,而不会使已经运行的代码产生任何问题。

实际上Rubinius这样做是很频繁的,因为垃圾回收器中有一种就是一个复制-压缩收集器,因此较新的对象要经常移动。如果你在一个对象上调用next_object,方法返回对象B,你绝不能指望在下次调用next_object的时候,返回的还是对象B。

与之相关的一件事是Rubinius目前已经完成了将对象的object_id和它在内存中的位置解耦。MRI的object_id返回对象在内存中的地址,这样之后,我们就可以通过调用ObjectSpace._id2ref,以object_id返回的数值作为参数,从而返回相应对象。在MRI中,_id2ref要实现起来相当容易,因为它只需直接从内存的相应位置取得对象就可以了。但是经常用到_id2ref的人知道,有时候你取出来的对象和原先放入的完全不同。这是因为旧的对象已经完成生命周期,而新的对象已经被分配在同一个地方。这虽然没有妨碍人们继续使用_id2ref,但人们应当清楚,_id2ref并不是像人们想象的那样工作的。

无论如何,Rubinius目前还不支持_id2ref,因为object_id保存的并不是内存地址。我们会找到支持的方式的,但和JRuby一样,这可能会成为一个彻底的额外性能开支。

查看英文原文:Evan Phoenix on Rubinius - VM Internals Interview

没有回复

回复

独家内容

专访开源项目Amoeba架构师陈思儒

DBA notes站长冯大辉(Fenng)代表InfoQ中文站采访了分布式数据库Proxy开源项目Amoeba的架构师和主要开发者陈思儒,内容包括Amoeba项目的起因、功能及其愿景等。

使用JSF、Ajax和Seam开发Portlets(2/3)

作为三期系列文章的第二部分,本文延续了上一期内容,介绍了RichFaces,包括如何把RichFaces集成到之前提到的示例应用中、如何部署RichFaces porlet和RichFaces的多种特性和功能。

Jeff Barr谈论Amazon Web服务

Amazon Web Services(AWS)的传道者Jeff Barr讨论了SimpleDB、S3、EC2、SQS、云计算、Amazon的不同服务如何与应用交互、AWS的起源、SimpleDB和微软SQL Server Data Services、AWS cloud的全球化、三月份的AWS停机。

用Erlang实现领域特定语言

Erlang的并发模型很有名,它的健壮性也很有名。但其他方面呢?在这篇文章里,Dennis Byrne演示了如何用Erlang建立内部DSL。

基于Rails的企业级应用剖析

本视频主要以FreeWheel为例,对一个基于Rails的企业级应用进行了剖析。其中包括:FreeWheel的架构、部署、数据库的问题、REST API、敏捷开发过程、如何去写测试以及持续集成等等。

JavaFX技术预览

JavaFX显示了Sun的Java系列产品市场方向的一个重大转变。随着1.0版的即将发布,InfoQ以JavaFX预览版为参考,与Sun高级工程师Joshua Marinacci探讨了即将发布的1.0正式版。

剖析短迭代

敏捷教练Dave Nicolette提出:我们应该如何设定迭代长度?是要根据发布周期的时间么?使用短迭代又有哪些好处?

应用JSF、Ajax和Seam开发Portlets(1/3)

本文主要讲述了如何用JBoss Portlet Container 和JBoss Portlet Bridge创建新项目,怎样配置一个JSF应用去使用JBoss Portlet Bridge,以及JBoss Portlet Bridge所具备的功能。