作者:eclipse
为性能而设计, 第一部分: 接口事宜
from java world.
[b]在设计 java 类的时候避免性能上的冒险[/b]
[b][u]概要[/u][/b]
许多通常的 java 性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者
开始考虑性能问题之前. 在这个系列中, brian goetz 讨论了通常的 java 性能上的冒险
以及怎么在设计时候避免它们.
by brian goetz
翻译 by supermmx
许多程序员在开发周期的后期才可是考虑性能管理. 他们常常把性能优化拖延到最后, 希
望能完全避免 -- 有时候这种策略是成功的. 但是早期的设计思想可以影响性能优化的需
求及其成功. 如果性能是你的程序的一个重要指标, 那么性能管理应该从第一天起就和设
开发周期整合在一起.
这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法. 在这篇文章中, 我
专注于最通常的性能问题中的一个: 临时变量的创建. 一个类的对象创建方式常常在设计
时候就确定了的 -- 但不是故意的 --, 就为后来的性能问题种下了种子.
阅读整个的 "为性能而设计" 系列:
第一部分: 接口事宜
第二部分: 减少对象创建
第三部分: 远程接口 (march 23, 2001)
性能问题有各种形式. 最容易调整的是那些你简单地为计算选择了一个错误的算法 --
就象使用使用冒泡算法来对一个大数据集进行排序, 或者在使用一个经常使用的数据项时
不是做缓冲, 而是每次都计算. 你可以使用概要分析来简单地找出这些瓶颈, 一旦找到了,
你可以很容易地改正. 但是, 许多 java 性能问题来自一个更深的, 更难改正的源头 --
一个程序组件的接口设计.
今天大多数程序是由内部开发的或者外部买来的组件构建而成. 甚至在程序不是很大地依
于已经存在的组件时, 面向对象的设计过程也鼓励应用程序包装成组件, 这样就简化了设
计, 开发和测试过程. 这些优势是不可否认的, 你应该认识到这些组件实现的接口可能
极大地影响使用它们的程序的行为和性能.
在这一点上, 你可能要问什么样的接口和性能相关. 一个类的接口不仅定义了这个类可
以实现那些功能, 也可以定义它的对象创建行为和使用它的方法调用序列. 一个类怎样
定义它的构造函数和方法决定了一个对象是否可以重用, 它的方法是否要创建 -- 或者
要求它的客户端创建 -- 中间对象, 以及一个客户端需要调用多少方法来使用这个类.
这些因素都会影响程序的性能.
[b][u]注意对象的创建[/u][/b]
一个最基本的 java 性能管理原则就是: 避免大量的对象创建. 这不是说你应该不创建
任何对象而放弃面向对象的好处. 但是你必须在执行性能相关的代码时, 在紧循环中注意
对象的创建. 对象的创建是如此地高代价, 以至于你应该在要求性能的情况下避免不必要
的临时或者中间对象的创建.
string 类是在那些处理文本的程序中对象创建的主要来源. 因为 string 是不可修改的,
每当一个 string 修改或创建, 就必须创建一个新的对象. 结果就是, 关注性能的程序应
该避免大量 string 的使用. 但是, 这通常是不可能的. 甚至当你从你的代码中完全除去
对 string 的依赖, 你常常会发现你自己在使用一些具有根据 string 定义的接口的组件.
所以, 你最后不得不使用 string.
[b][u]例子: 正规表达式匹配[/u][/b]
作为一个例子, 假设你写一个叫做 mailbot 的邮件服务器. mailbot 需要处理 mime 头格
式 -- 象发送日期或者发送者的 email 地址 -- 在每个信息的顶部. 使用一个匹配正规
表达式的组件来使处理 mime 头的过程简单一些. mailbot 足够聪明, 不为每个头的行
或者头的元素创建一个 string 对象. 相反, 它用输入的文本填充了一个字符缓冲区, 通
过对缓冲区的索引来确定要处理的头的位置. mailbot 会调用正规表达式匹配器来处理每
个头行, 所以匹配器的性能就非常重要. 我们以一个正规表达式匹配器类的拙劣的接
口作为例子:
public class awfulregexpmatcher {
/** create a matcher with the given regular expression and which will
operate on the given input string */
public awfulregexpmatcher(string regexp, string inputtext);
/** retrieve the next match of the pattern against the input text,
returning the matched text if possible or null if not */
public string getnextmatch();
}
甚至在这个类实现了一个有效的正规表达式匹配的算法的时候, 任何大量使用它的程序
仍然难以忍受. 既然匹配器对象和输入的文本联系起来, 每一次你调用它, 你必须创建
一个新的匹配器对象. 既然你的目标是减少不必要的对象的创建, 那么使这个匹配器可以赜
将会是一个明显的开始.
下面的类定义演示了你的匹配器的另一个可能的接口, 允许你重用这个匹配器, 但仍然
很坏.
public class badregexpmatcher {
public badregexpmatcher(string regexp);
/** attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public string match(string inputtext);
/** get the next match against the input text, or return null if no match */
public string getnextmatch();
}
忽略正规表达式匹配中的精细点 -- 象返回匹配的子表达式, 这个看起来无害的类定义
会出什么问题呢? 从功能上来看, 没有. 但是从性能的角度来看, 许多. 首先, 匹配器
需要它的调用者创建一个 string 来代表要匹配的文本. mailbot 试图避免创建 string
对象, 但是当它要找到一个要做正规表达式解析的头时, 它不得不创建一个 string 来
满足 badregexpmatcher:
badregexpmatcher datematcher = new badregexpmatcher(...);
while (...) {
...
string headerline = new string(mybuffer, thisheaderstart,
thisheaderend-thisheaderstart);
string result = datematcher.match(headerline);
if (result == null) { ... }
}
第二, 匹配器创建了结果字符串甚至当 mailbot 只关心是否匹配了, 不需要匹配的文本时,
这意味着要简单使用 badregexpmatcher 来确认一个日期头是否匹配一个特定的格式, 你
必须创建两个 string 对象 -- 匹配器的输入和匹配的结果. 两个对象可能看起来不多,
但是如果你给 mailbot 处理的每个邮件的每个头行都创建两个对象, 这会极大地影响
性能. 错误不在于 mailbot 的设计, 而在于 badregexpmatcher 类的设计 -- 或者使用.
注意返回一个轻量型的 match 对象 -- 可以提供 getoffset(), getlength(),
egetmatchstring() 方法 -- 而不是返回一个 string, 这不会很大提高性能. 因为创建
一个 match 对象可能比创建一个 string 代价要小 -- 包括产生一个 char[] 数组和
复制数据, 你仍然创建了一个中间对象, 对你的调用者来说没有价值.
这已经足够坏了, badregexpmatcher 强迫你使用它想看到的输入形式, 而不是你可以
提供的更有效的形式. 但是使用 badregexpmathcer 还有另一个危险, 潜在地给 mailbot
的性能带来更大的冒险: 在处理邮件头的时候, 你开始有避免使用 string 的倾向. 但是
既然你被迫创建许多 string 对象来满足 badregexpmatcher, 你可能被引诱而放弃这个
目标, 更加自由地使用 string. 现在, 一个组件的糟糕的设计已经影响了使用它的程序.
甚至你后来找到了一个更好的正规表达式的组件, 不需要你提供一个 string, 那时你的
整个程序都会受影响.
[b][u]一个好一些的接口[/u][/b]
你怎样定义 badregexpmatcher, 而不引起这样的问题呢? 首先, badregexpmatcher 应该
不规定它的输入. 它应该可以接受它的调用者能够有效提供的各种输入格式. 第二, 它
不应该自动给匹配结果产生一个 string; 应该返回足够的信息, 这样调用者如果愿意的
话可以生成它. (为方便着想, 它可以提供一个方法来做这件事, 但不是必须的) 这里
有一个好一些的接口:
class betterregexpmatcher {
public betterregexpmatcher(...);
/** provide matchers for multiple formats of input -- string,
character array, and subset of character array. return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(string inputtext);
public int match(char[] inputtext);
public int match(char[] inputtext, int offset, int length);
/** get the next match against the input text, if any */
public int getnextmatch();
/** if a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the m