Performance Tips I
假如所有的人都很在意高性能代码的话,那么我的这篇Blog就没有意义了。我希望能够给出一些有意义的性能提示,这些是我们日常编码工作中总会碰到的,所以对这些小tips漠不关心只会日益加深对你代码的伤害,而且做Review的时候也会很不体面,老实说,一个人写的代码确切地反映了这个人的能力与个性,对于经常看不同代码的我们来说,这并不是胡说的,我们有确切的体会。
首先值得一提的是,如果你的程序有性能问题,请不要首先在代码中找,因为一般情况下,大的性能问题都发生在架构与设计上,架构与设计的性能问题是在代码层次解决不了的,影响面也很广。这篇文章不会告诉你如何在架构与设计中找性能问题,或者架构与设计的性能提示。这篇文章只是在编码层次上给这个层次的性能提示,虽然比不上架构与设计的性能优化,但是如果不注意也还是会带来显著的性能影响的。
我会以总结的方式列出每条我能想到的性能提示来,并解析为什么会这样,希望对读者有用。
Resource Management(资源管理)
作为任何一个程序,都需要考虑它所能使用的资源界限以及它目前使用的资源状况。假设一个程序只被允许使用100MB内存,但是它在执行某个大任务时内存占用却达到了400MB,那么这时你就会发现你有个不得不解决的大问题。任何硬件的资源都是有限的(不要跟我说SETI,没有几个程序是SETI类型的),作为程序,如何利用好这些有限的资源达到目的是任何一个开发人员不得不考虑的(不管你是架构师、设计师还是纯粹的Developer)。首先我们要知道所谓的资源其实可以分为几大类:- CPU
- Memory
- Disk I/O
- Network I/O
为了有效的利用这些有限的资源,我们有时不得不针对资源管理做一些优化,下面的这些便是一些相关的tips。可能你会问:“资源的管理.NET下不是自动的嘛,不是有GC的嘛”,没错,GC是做资源管理的,但是这并不代表有GC我们就不用注意资源的使用了,因为GC的行为是固定的,所以我们可以针对GC的行为来为它优化。下面会有几条tips谈到。
只在你真正需要时分配内存。
为什么:
内存的过早分配会造成不必要的效率损失以及资源紧缺或者浪费。
不好的做法:
// Result对象过早的分配,当a/b发生异常时
// 这个对象就白分配了,因为这个对象只有被
// 方法返回才有意义。
Result r = new Result();
r.Value = a / b;
return r;
}
好的做法:
double value = a / b;
Result r = new Result();
r.Value = value;
return r;
}
尽可能早的释放资源。
为什么:
对于你已经确定不再需要的对象如果不尽早对其释放就有可能造成不必要的资源紧缺。
不好的做法:
connection.Open();
try {
// Data operations
// PostProcessing并不涉及数据库连接的使用,
// 在处理PostProcessing的时候,一个宝贵的
// 数据库连接资源就被这样白白的浪费了,如果
// 这时有其他需要数据库连接的操作而它又
// 没有可用连接那么就会引发timeout等等因数据库
// 连接不足而产生的各种问题。
PostProcessing();
} finally {
connection.Close();
}
}
好的做法:
connection.Open();
try {
// Data operations
} finally {
connection.Close();
}
PostProcessing();
}
不要使用Finalizer,除非是为了确保关键资源的正确释放。
为什么:
有Finalizer的对象需要两次GC才能被彻底回收,影响内存回收效率,第一次GC会执行Finalizer,第二次才会释放对象所占用的内存空间。
不好的做法:
// 影响资源回收的速度。
instanceNumber--;
}
好的做法:
对于拥有非托管资源的对象,一定要实现Disposable模式。
为什么:
实现了整个Disposable模式才能确保非托管资源会被正确释放(非托管资源不受GC管理,需手动释放),并且提供了手动释放的方法。不好的做法:
// 定义关闭这个非托管资源的方法。
class TextFile {
private FileStream fileStream;
public string ReadLine() {
}
}
好的做法:
private FileStream fileStream;
public string ReadLine() {
}
public void Dispose() {
this.Dispose(true);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
// 在这儿清理一切托管的与Disposable的托管
// 成员。
if (this.fileStream != null) {
this.fileStream.Close();
this.fileStream = null;
}
// 因为我们已经显示地清理了资源,所以不再需要
// Finalizer了,有它会影响效率。
GC.SupressFinalize();
}
// 在这儿清理一切非托管资源。
// 如果有基类,应在这里调用基类的Dispose。这样做
// 是因为清理顺序应该是首先清理自身的一切资源,
// 然后基类清理自身的一切资源,依此类推。
}
~TextFile() {
this.Dispose(false);
}
}
在Dispose方法中调用GC.SupressFinalize。
为什么:
就像前面提到的,有Finalizer会影响回收效率,既然你都已经手动Dispose了,那么干吗还要Finalizer呢(注意Finalizer的作用应该限制在调用Dispose上)?
不好的做法:
请参照上一示例。
好的做法:
请参照上一示例。
避免延长存活期短的对象的寿命。
为什么:
存活期短的对象会被GC优先回收,回收的效率更高。如果无谓的延长它的寿命的话再次回收它时效率会更低,而且它也会被更慢的回收。GC回收时是按照generation来回收,新创建的对象都在generation 0中,GC回收时先回收generation 0中的对象,如果回收到足够的空间那么将不再继续回收,如果不够再在generation 1中回收,依此类推,最高的generation是2,每次回收后存活下来的对象将升至更高级别的generation,从0到1再到2。寿命短的对象很自然的会在generation 0中回收,但如果延长它的寿命的话它就很可能会升至generation 1,这样一来就没有generation 0的回收效率了。
不好的做法:
string lines = "";
for (int i = 1; i <= 10; i++) {
// 每次lines的临时内容都不能第一次就被
// 回收,因为lines有引用,所以这个临时对象
// 会被promote为更高的generation,这样无形
// 中就对GC的工作压力产生了巨大的影响。
lines += "Line " + i + Environment.NewLine;
}
Console.Write(lines);
}
好的做法:
在这个特殊的例子中应使用StringBuilder避免临时对象的产生。
注意字符串连接的做法。
为什么:
不同的字符串连接需采用不同的方法,这样才能达到不损伤性能。有以下几种情况:
- 连接string literal:采用+符号连接,编译时编译器会将这些被连接的literals自动写成一个literal。
- 一次性连接已知数量的literal与/或变量:采用+符号连接,因为这个连接其实只是一个String.Concat,没有临时对象产生。
- 多次连接或者连接未知数量的literal或变量:采用StringBuilder来追加内容,避免临时对象的产生。
不好的做法:
// Case 1. 连接literals使用StringBuilder
// 要比用+号性能低很多。
StringBuilder builder = new StringBuilder();
builder.Append("This is a very very ");
builder.Append("very very very very ");
builder.Append("very very very very ");
builder.Append("very very very very ");
builder.Append("long string literal");
Console.Write(builder.ToString());
// Case 2. 一次性连接已知数量就足够的情况下
// 就不必使用StringBuilder了。
StringBuilder builder2 = new StringBuilder();
builder2.Append("Literal variable newline");
builder2.Append(tempVar);
builder2.Append(Environment.NewLine);
Console.Write(builder2.ToString());
// Case 3.1. 这里每次+=都会产生两个临时字符串
// 对象,大大增加了GC的负担,严重影响了效率。
string lines = "";
for (int i = 1; i <= 10; i++) {
lines += "Line " + i + Environment.NewLine;
}
Console.Write(lines);
// Case 3.2. 这里每次Append时会由+引起临时字符串
// 对象,影响效率。
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 10; i++) {
builder.Append("Line " + i + Environment.NewLine);
}
Console.Write(builder.ToString());
}
好的做法:
// Case 1.
string longLiteral =
"This is a very very " +
"very very very very " +
"very very very very " +
"very very very very " +
"long string literal";
Console.Write(longLiteral);
// Case 2.
string line =
"Literal variable newline" +
tempVar + Environment.NewLine;
Console.Write(line);
// Case 3.
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 10; i++) {
builder.Append("Line ");
builder.Append(i);
builder.Append(Environment.NewLine);
}
Console.Write(builder.ToString());
}
不采用ToUpper或ToLower进行字符串不区分大小写的对比。
为什么:
ToUpper与ToLower每次对比时会产生临时对象,影响性能。改用String.Compare,这不会有临时对象产生,而且还可以根据文化进行对比(或文化中立的进行对比)。
不好的做法:
// 这里会有两个临时字符串对象产生。
if (s1.ToLower() == s2.ToLower()) {
}
}
好的做法:
if (string.Compare(s1, s2, false)) {
}
}
To be continued...