« 新闻从业者的素质 | 返回首页 | 闲话 java »

给脚本加入字符串类型

最近的工作是给虚拟机加上字符串类型的支持,并让编译器可以生成相应的 bytecode 。思路很简单,就是按 lua 的方式,把所有源码中相同的字符串合并,在 bytecode 中只保留一份。所有提到这些字符串的地方直接对其引用。bytecode运行时,产生的任何新的字符串都会产生新的副本。垃圾等到 gc 时回收。

想起来容易,实现起来还是颇为麻烦的。

首先要解决的是,bytecode在内存时,所有提到的 string 都可以直接是一个地址。但是 bytecode 在没有加载到虚拟机前,任何绝对地址都是不允许的。所以加载 byecode 的过程,不再是简单的内存复制。对 string 做操作时,希望尽量是 0 消耗,不希望出现任何惰性初始化的过程。

现在的解决方案是,扫描源码的过程(我的编译器是一遍扫描的),碰到任何的字符串都被编号。在函数定义中,直接把编号压栈。在非函数定义时,则按编号在一个特定的局部table中取出来。非函数内代码,操作 string 是有消耗的,有一次取 table 的过程。不过因为局部 table 读操作也是一行 bytecode 所以空间消耗没有增加。在注册函数时,扫描函数体的 bytecode 转换所有的编号到绝对地址,待到调用时,就是 0 消耗了。

把全部源码编译完后,自动生成一个注册函数,将所有提到的 string 注册到虚拟机中。最后在生成的 bytecode 最前插入一行调用这个自动生成函数。

自己的方案简述完了。至于虚拟机内的处理也是比较麻烦,不过只是麻烦而已,代码还是一样的要写下去的。
这里闲话几句 java 。

java 中的 string 在虚拟机实现的时候跟 lua 这个方案类似。但是许多 java 程序员并不太知晓。一个事件是从李维那听来的,据说是 tomcat 4 里的问题(我有点记不清版本了),在一个内循环中使用了 string += 的操作。结果这个操作被运行了很次后产生了大量的垃圾,致使 gc 频繁调用。

另一个事件是听我的同事说的,说我们公司另一个主用java做项目的部门的人,在一个项目中用到了持久化的操作。在做 codeview 的时候,我那个同事置疑其效率。然后就有人回去做测试,得出的结论是,java 在<strike>持久化</strike> 序列化 string 的时候效率颇高,几乎没有效率问题。当时听到这里我就笑了,虽然我没写过 java 程序,但是直觉告诉我,某人一定用相同的字符串,放到一个循环中去持久化了 :D 难怪事后得出的结论是,java 持久化不仅效率高,还带有压缩功能。

12月8日补遗:
下面有朋友谈到应该是序列化而不是持久化,吾以为然也。就这个问题上看,我们公司的 java 程序员想用这个功能,把一组 string 写到硬盘上,做的是一个持久化操作。作为 java 程序员的一种共性,他们不会去考虑 IO 操作的消耗,序列化数据的消耗等。但是转述这个故事和听故事的我,都是 C++ 程序员,第一反应却是性能的问题。(当时这个是用在一个性能要求很高的应用中)
至于 string += 的操作,对虚拟机实现用一定了解的程序员会很敏感于内存消耗。当时李维举这个例子是想说明有些 java 程序员太不关心 java 的语言细节和性能。当然正确的做法,java 老手都应该明白,我这个不懂 java 的 C++ 程序员也可以猜到。但是错误发生在 TomCat 正式发布版中的确不应当。

Comments

一遍扫描的编译器若用协程实现,可以用三遍扫描的逻辑实现事实上一遍扫描的程序。有没有考虑使用呢?
一遍扫描的编译器
string += 也是New String了
所以说自己实现虚拟机可以学到很多东西,是触类旁通的。lua1.0写的也很简陋,也用了lex/yacc,别人从93年就开始积累到现在,难怪水平高
在Java中,String是一个不可变的对象,也就是一旦生成便无法修改了。所以,像云风提到的那种“+”操作,每次都会产生一个新的对象。在Java中,标准做法是使用StringBuffer。 Java持久化?是序列化(Serialization)吧?^_^
.NET中字符串的 + 运算符,在编译的时候可能转换为Format http://blog.joycode.com/jgtm2000/archive/2004/04/05/18392.aspx
哈哈

Post a comment

非这个主题相关的留言请到:留言本