发信人: SuperMMX (笑天子*不再喝可乐)
发信站: BBS 水木清华站
From http://SuperMMX.dhs.org/forum
原文请到此站查看.
为性能而设计, 第一部分: 接口事宜
From Java World.
在设计 Java 类的时候避免性能上的冒险
概要
许多通常的 Java 性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者
开始考虑性能问题之前. 在这个系列中, Brian Goetz 讨论了通常的 Java 性能上的冒险
以及怎么在设计时候避免它们.
By Brian Goetz
翻译 by SuperMMX
许多程序员在开发周期的后期才可是考虑性能管理. 他们常常把性能优化拖延到最后, 希
望能完全避免 -- 有时候这种策略是成功的. 但是早期的设计思想可以影响性能优化的需
求及其成功. 如果性能是你的程序的一个重要指标, 那么性能管理应该从第一天起就和设
开发周期整合在一起.
这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法. 在这篇文章中, 我
专注于最通常的性能问题中的一个: 临时变量的创建. 一个类的对象创建方式常常在设计
时候就确定了的 -- 但不是故意的 --, 就为后来的性能问题种下了种子.
阅读整个的 "为性能而设计" 系列:
第一部分: 接口事宜
第二部分: 减少对象创建
第三部分: 远程接口 (March 23, 2001)
性能问题有各种形式. 最容易调整的是那些你简单地为计算选择了一个错误的算法 --
就象使用使用冒泡算法来对一个大数据集进行排序, 或者在使用一个经常使用的数据项时
不是做缓冲, 而是每次都计算. 你可以使用概要分析来简单地找出这些瓶颈, 一旦找到了,
你可以很容易地改正. 但是, 许多 Java 性能问题来自一个更深的, 更难改正的源头 --
一个程序组件的接口设计.
今天大多数程序是由内部开发的或者外部买来的组件构建而成. 甚至在程序不是很大地依
于已经存在的组件时, 面向对象的设计过程也鼓励应用程序包装成组件, 这样就简化了设
计, 开发和测试过程. 这些优势是不可否认的, 你应该认识到这些组件实现的接口可能
极大地影响使用它们的程序的行为和性能.
在这一点上, 你可能要问什么样的接口和性能相关. 一个类的接口不仅定义了这个类可
以实现那些功能, 也可以定义它的对象创建行为和使用它的方法调用序列. 一个类怎样
定义它的构造函数和方法决定了一个对象是否可以重用, 它的方法是否要创建 -- 或者
要求它的客户端创建 -- 中间对象, 以及一个客户端需要调用多少方法来使用这个类.
这些因素都会影响程序的性能.
注意对象的创建
一个最基本的 Java 性能管理原则就是: 避免大量的对象创建. 这不是说你应该不创建
任何对象而放弃面向对象的好处. 但是你必须在执行性能相关的代码时, 在紧循环中注意
对象的创建. 对象的创建是如此地高代价, 以至于你应该在要求性能的情况下避免不必要
的临时或者中间对象的创建.
String 类是在那些处理文本的程序中对象创建的主要来源. 因为 String 是不可修改的,
每当一个 String 修改或创建, 就必须创建一个新的对象. 结果就是, 关注性能的程序应
该避免大量 String 的使用. 但是, 这通常是不可能的. 甚至当你从你的代码中完全除去
对 String 的依赖, 你常常会发现你自己在使用一些具有根据 String 定义的接口的组件.
所以, 你最后不得不使用 String.
例子: 正规表达式匹配
作为一个例子, 假设你写一个叫做 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, 那时你的
整个程序都会受影响.
一个好一些的接口
你怎样定义 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 match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
新的接口减少了调用者把输入转换成匹配器希望的格式这个要求. MailBot 现在可以象
下面这样调用 match():
int resultOffset = dateMatcher.match(myBuffer, thisHeaderStart,
thisHeaderEnd-thisHeaderStart);
if (resultOffset < 0) { ... }
这就解决了不创建任何新对象的目标. 作为一个附加的奖励, 它的接口设计风格加到了
Java 的 "lots-of-simgle-methos" 设计哲学中.
额外的对象创建给性能的确切的冲击依赖于 matth() 所作的工作量. 你可以通过创建和计时
两个正规表达式匹配器类, 来确定一个性能差别的上限. 在 Sun JDK 1.3 中, 上面的代码
片段在 BetterRegExpMatcher 类中大约比 BadRegExpMatcher 类要快 50 倍左右. 使用一个
简单的字串匹配的实现, BetterRegExpMatcher 比 相对应的 BadRegExpMatcher 要快 5 ?
交换类型
BadRegExpMatcher 强迫 MailBot 把输入文本从字符数组转换成 String, 结果是造成了
一些不必要的对象的创建. 更具讽刺意味的是, BadRegExpMatcher 的许多实现都立即
把 String 转换成一个字符数组, 使它容易对输入文本进行访问. 这样不仅仅申请了另一龆
象, 并且还意味着你做完了所有的工作, 最后的形式和开始时一样. MailBot 和 BadRegExpMatcher
都不想处理 String -- String 只是看起来象是在组件之间传递文本的很明显的格式.
在上面的 BadRegExpMatcher 例子中, String 类是作为一个交换类型的. 一个交换类型
是一种不管是调用者还是被调用者都不想使用或者以它作为数据格式的一种类型, 但是
两个都能很容易地转换它或者从它转换. 以交换类型定义接口在保持灵活性的同时减少了
接口的复杂性, 但是有时简单性导致了高代价的性能.
一个交换类型最典型的例子是 JDBC ResultSet 接口. 它不可能象任何本地数据库提供的
数据集一样提供它的 ResultSet 接口, 但是 JDBC 驱动通过实现一个 ResultSet 可以很
容易地把数据库提供的本地数据表示包装起来. 同样, 客户端程序也不能象这样表示数据
记录, 但是你几乎可以没有困难地把 ResultSet 转换为想要的数据表示. 在 JDBC 的例子中,
你接受了这个层次的花费, 因为它带来了标准化和跨数据库实现的可移植性的好处. 但是,
要注意交换类型带来的性能代价.
这完全不值得, 使用交换类型对性能的冲击不容易度量. 如果你对上面调用 BadRegExpMatcher
的代码片段做测试的话, 它会在运行时创建 MailBot 的输入 String; 但是, String 的产生
只用来满足 BadRegExpMatcher. 如果你想评定一个组件对程序性能的真正的冲击, 你应该不仅
仅度量它的代码的资源使用状况, 还有那些使用它和恢复的代码. 这对于标准的测试工具此
很难完成.
结论
不是所有的程序都关注于性能的, 不是所有的程序都有性能问题. 但是对那些关注这些
的程序, 这篇文章所提到的都很重要, 因为它们不是在最后一分钟就可以修改的. 既然在
你编写写代码使用一个类以后再修改它的接口非常困难, 那么在你的设计时期就花费一点
额外的时间来考虑性能特性.
在第二部分, 我会演示一些利用可修改性和不可修改性来减少不必要的对象创建的方法.
About the author
Brian Goetz is a professional software developer with over 15 years of experience.
He is a principal consultant at Quiotix, a software development and consulting
firm located in Los Altos, Calif.