1.7.3 避免两次检索集合元素
获取集合元素时,有时需要检查元素是否存在。通常的做法是先调用ContainsKey(或Contains)方法,然后再获取集合元素。这种写法非常符合逻辑。
但如果考虑效率,可以先直接获取对象,然后判断对象是否为null来确定元素是否存在。对于Hashtable,这可以节省一次GetHashCode调用和n次Equals比较。
如下面的示例:
public IData GetItemByID(Guid id)
{
IData data1 = null ;
if ( this .idTable.ContainsKey(id.ToString())
{
data1 = this .idTable[id.ToString()] as IData;
}
return data1;
}
其实完全可用一行代码完成:return this.idTable[id] as IData;
1.7.4 避免两次类型转换
考虑如下示例,其中包含了两处类型转换:
if (obj is SomeType)
{
SomeType st = (SomeType)obj;
st.SomeTypeMethod();
}
效率更高的做法如下:
SomeType st = obj as SomeType;
if (st != null )
{
st.SomeTypeMethod();
}
1.8 Hashtable
Hashtable是一种使用非常频繁的基础集合类型。需要理解影响 Hashtable的效率有两个因素:一是散列码(GetHashCode方法),二是等值比较(Equals方法)。Hashtable首先使用键的散列码将对象分布到不同的存储桶中,随后在该特定的存储桶中使用键的Equals方法进行查找。
良好的散列码是第一位的因素,最理想的情况是每个不同的键都有不同的散列码。Equals方法也很重要,因为散列只需要做一次,而存储桶中查找键可能需要做多次。从实际经验看,使用Hashtable时,Equals方法的消耗一般会占到一半以上。
System.Object类提供了默认的GetHashCode实现,使用对象在内存中的地址作为散列码。我们遇到过一个用Hashtable来缓存对象的例子,每次根据传递的OQL表达式构造出一个ExpressionList对象,再调用 QueryCompiler的方法编译得到CompiledQuery对象。以ExpressionList对象和CompiledQuery对象作为键值对存储到Hashtable中。ExpressionList对象没有重载GetHashCode实现,其超类ArrayList也没有,这样最后用的就是System.Object类的GetHashCode实现。由于ExpressionList对象会每次构造,因此它的HashCode每次都不同,所以这个CompiledQueryCache根本就没有起到预想的作用。这个小小的疏漏带来了重大的性能问题,由于解析OQL表达式频繁发生,导致 CompiledQueryCache不断增长,造成服务器内存泄漏!解决这个问题的最简单方法就是提供一个常量实现,例如让散列码为常量0。虽然这会导致所有对象汇聚到同一个存储桶中,效率不高,但至少可以解决掉内存泄漏问题。当然,最终还是会实现一个高效的GetHashCode方法的。
以上介绍这些Hashtable机理,主要是希望大家理解:如果使用Hashtable,你应该检查一下对象是否提供了适当的GetHashCode和Equals方法实现。否则,有可能出现效率不高或者与预期行为不符的情况。
2. Ado.Net
2.1 应用Ado.net的一些思考原则
1. 根据数据使用的方式来设计数据访问层
2. 缓存数据,避免不必要的操作
3. 使用服务帐户进行连接
4. 必要时申请,尽早释放
5. 关闭可关闭的资源
6. 减少往返
7. 仅返回需要的数据
8. 选择适当的事务类型
9. 使用存储过程
2.2 Connection
数据库连接是一种共享资源,并且打开和关闭的开销较大。Ado.net默认启用了连接池机制,关闭连接不会真的关闭物理连接,而只是把连接放回到连接池中。因为池中共享的连接资源始终是有限的,如果在使用连接后不尽快关闭连接,那么就有可能导致申请连接的线程被阻塞住,影响整个系统的性能表现。
2.2.1 在方法中打开和关闭连接
这个原则有几层含义:
1. 主要目的是为了做到必要时申请和尽早释放
2. 不要在类的构造函数中打开连接、在析构函数中释放连接。因为这将依赖于垃圾回收,而垃圾回收只受内存影响,回收时机不定
3. 不要在方法之间传递连接,这往往导致连接保持打开的时间过长
这里强调一下在方法之间传递连接的危害:曾经在压力测试中遇到过一个测试案例,当增大用户数的时候,这个案例要比别的案例早很久就用掉连接池中的所有连接。经分析,就是因为A方法把一个打开的连接传递到了B方法,而B方法又调用了一个自行打开和关闭连接的C方法。在 A方法的整个运行期间,它至少需要占用两条连接才能够成功工作,并且其中的一条连接占用时间还特别长,所以造成连接池资源紧张,影响了整个系统的可伸缩性!
2.2.2 显式关闭连接
Connection对象本身在垃圾回收时可以被关闭,而依赖垃圾回收是很不好的策略。推荐使用using语句显式关闭连接,如下例:
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
} // Dispose is automatically called on the conn variable here
2.2.3 确保连接池启用
Ado.net是为每个不同的连接串建立连接池,因此应该确保连接串不会出现与具体用户相关的信息。另外,要注意连接串是大小写敏感的。
2.2.4 不要缓存连接
例如,把连接缓存到Session或Application中。在启用连接池的情况下,这种做法没有任何意义。
2.3 Command
2.3.1 使用ExecuteScalar和ExecuteNonQuery
如果想返回像Count(*)、Sum(Price)或Avg(Quantity)那样的单值,可以使用ExecuteScalar方法。 ExecuteScalar返回第一行第一列的值,将结果集作为标量值返回。因为单独一步就能完成,所以ExecuteScalar不仅简化了代码,还提高了性能。
使用不返回行的SQL语句时,例如修改数据(INSERT、UPDATE或DELETE)或仅返回输出参数或返回值,请使用ExecuteNonQuery。这避免了用于创建空DataReader的任何不必要处理。
2.3.2 使用Prepare
当需要重复执行同一SQL语句多次,可考虑使用Prepare方法提升效率。需要注意的是,如果只是执行一次或两次,则完全没有必要。例如:
cmd.CommandText = "insert into Table1 ( Col1, Col2 ) values ( @val1, @val2 )";
cmd.Parameters.Add( "@val1", SqlDbType.Int, 4, "Col1" );
cms.Parameters.Add( "@val2", SqlDbType.NChar, 50, "Col2");
cmd.Parameters[0].Value = 1;
cmd.Parameters[1].Value = "XXX";
cmd.Prepare();
cmd.ExecuteNonQuery();
cmd.Parameters[0].Value = 2;
cmd.Parameters[1].Value = "YYY";
cmd.ExecuteNonQuery();
cmd.Parameters[0].Value = 3;
cmd.Parameters[1].Value = "ZZZ";
cmd.ExecuteNonQuery();
2.3.3 使用绑定变量 ★
SQL语句需要先被编译成执行计划,然后再执行。如果使用绑定变量的方式,那么这个执行计划就可以被后续执行的SQL语句所复用。而如果直接把参数合并到了SQL语句中,由于参数值千变万化,执行计划就难以被复用了。例如上面Prepare一节给出的示例,如果把参数值直接写到insert语句中,那么上面的四次调用将需要编译四次执行计划。
为避免这种情况造成性能损失,要求一律使用绑定变量方式。
2.4 DataReader
DataReader最适合于访问只读的单向数据集。与DataSet不同,数据集并不全部在内存中,而是随不断发出的read请求,一旦发现数据缓冲区中的数据均被读取,则从数据源传输一个数据缓冲区大小的数据块过来。另外,DataReader保持连接,DataSet则与连接断开。
2.4.1 显式关闭DataReader
与连接类似,也需要显式关闭DataReader。另外,如果与DataReader关联的Connection仅为DataReader服务的话,可考虑使用Command对象的ExecuteReader(CommandBehavior.CloseConnection)方式。这可以保证当 DataReader关闭时,同时自动关闭Connection。
2.4.2 用索引号访问代替名称索引号访问属性
从Row中访问某列属性,使用索引号的方式比使用名称方式有细微提高。如果会被频繁调用,例如在循环中,那么可考虑此类优化。示例如下:
cmd.CommandText = "select Col1, Col2 from Table1" ;
SqlDataReader dr = cmd.ExecuteReader();
int col1 = dr.GetOrdinal("Col1");
int col2 = dr.GetOrdinal("Col2");
while (dr.Read())
{
Console.WriteLine( dr[col1] + "_" + dr[col2]);
}
2.4.3 使用类型化方法访问属性
从Row中访问某列属性,用GetString、GetInt32这种显式指明类型的方法,其效率较通用的GetValue方法有细微提高,因为不需要做类型转换。
2.4.4 使用多数据集
部分场景可以考虑一次返回多数据集来降低网络交互次数,提升效率。示例如下:
cmd.CommandText = "StoredProcedureName"; // The stored procedure returns multiple result sets.
SqlDataReader dr = cmd.ExecuteReader();
while (dr.read())
// read first result set
dr.NextResult();
while (dr.read())
//
2.5 DataSet
2.5.1 利用索引加快查找行的效率
如果需要反复查找行,建议增加索引。有两种方式:
1. 设置DataTable的PrimaryKey
适用于按PrimaryKey查找行的情况。注意此时应调用DataTable.Rows.Find方法,一般惯用的Select方法不能利用索引。
2. 使用DataView
适用于按Non-PrimaryKey查找行的情况。可为DataTable创建一个DataView,并通过SortOrder参数指示建立索引。此后使用Find或FindRows查找行。
3.1 减少往返行程(Reduce Round Trips)
使用下面的方法可以减少Web服务器和Browser之间的往返行程:
1. 为Browser启用缓存
如果呈现的内容是静态的或变化周期较长,应启用Browser缓存,避免发出冗余的http请求。
2. 缓冲页面输出
如果可能,则尽量缓冲页面输出,处理结束后再一次传送到客户端,这可以避免频繁传递小块内容所造成的多次网络交互。由于这种方式在页面处理结束之前客户端无法看到页面内容,因此如果一个页面的尺寸较大的话,可考虑使用 Response.Flush方法。该方法强制输出迄今为止在缓冲区中的内容,你应当采用合理的算法控制调用Response.Flush方法的次数。
3. 使用Server.Transfer重定向请求
使用Server.Transfer方法重定向请求优于Response.Redirect方法。原因是Response.Redirect会向Broswer回送一个响应头,在响应头中指出重定向的 URL,之后Brower使用新的URL重新发出请求。而Server.Transfer方法直接是一个简单的服务端调用,完全没有这些开销!
需要注意Server.Transfer有局限性:第一,它会跳过安全检查;第二,只适用于在同一Web应用内的页面间跳转。
3.2 避免阻塞和长时间的作业
如果需要运行阻塞或长时间运行的操作,可以考虑使用异步调用的机制,以便Web服务器能够继续处理其它的请求。
1. 使用异步方式调用Web服务和远程对象
只要有可能就要避免在请求的处理过程中对Web服务和远程对象的同步调用,因为它占用的是的ASP.NET 线程池中的工作线程,这将直接影响Web服务器响应其它请求的能力。
2. 考虑给不需要返回值的Web方法或远程对象的方法添加OneWay属性
这种模式能让Web Server调用之后就立即返回。可根据实际情况决定是否使用这种方法。
3. 使用工作队列
将作业提交到服务器上的工作队列中。客户端通过发送请求来轮询作业的执行结果。
3.3 使用缓存
缓存能在很大程度上决定ASP.NET应用的最终性能。Asp.net支持页面输出缓存和页面部分缓存,并提供Cache API,供应用程序缓存自己的数据。是否使用缓存可考虑下面的要点:
1. 识别创建与访问代价较大的数据
2. 评估需要缓存数据的易变性
3. 评估数据的使用频次
4. 将要缓存数据中易变数据和不变数据分离,只缓存不变数据
5. 选择合适的缓存机制(除Asp.net Cache外,Application state和Session state也可以作为缓存使用)
3.4 多线程
1. 避免在请求处理过程中创建线程
在执行请求的过程中创建线程是一种代价较大的操作,会严重影响Web Server的性能。如果后续的操作必须用线程完成,建议通过thread pool来创建/管理线程。
2. 不要依赖线程数据槽或线程静态变量
由于执行请求的线程是ASP.NET thread pool中的工作线程,同一个Client的两次请求不一定由相同的线程来处理。
3. 避免阻塞处理请求的线程
参考"避免阻塞和长时间的作业"小节。
4. 避免异步调用
这和1的情况类似。异步调用会导致创建新的线程,增加服务器的负担。所以,如果没有并发的作业要执行,就不要执行异步调用。
3.5 系统资源
1. 考虑实现资源池以提升性能
2. 明确地调用Dispose或Close释放系统资源
3. 不要缓存或长时间占用资源池中的资源
4. 尽可能晚的申请,尽可能早的释放
3.6 页面处理
1. 尽量减小Page的尺寸
包括缩短控件的名称、CSS的class的名称、去掉无谓空行和空格、禁用不需要的ViewState
2. 启用页面输出的缓冲区(Buffer)
如果Buffer的机制被关闭,可以用下面的方法打开。
使用程序打开页面输出缓存:
Response.BufferOutput = true;
使用@Page开关打开页面输出缓冲机制:
<%@ Page Buffer = "true" %>
使用Web.config或Machine.config配置文件的<pages>节点:
<pages buffer="true" …>
3. 利用Page.IsPostBack优化页面输出
4. 通过分离页面的不同的内容,来提高缓存效率和减少呈现的时间
5. 优化复杂和代价较大的循环
6. 合理利用客户端的计算资源,将一些操作转移到客户端进行
3.7 ViewState
ViewState是Asp.net为服务端控件在页面回传之间跟踪状态信息而设计的一种机制。
1. 关闭ViewState
如果不需要跟踪页面状态,例如页面不会回传(PostBack)、不需要处理服务端控件事件或者每次页面刷新时都会重新计算控件内容,那么就不需要用ViewState来记录页面状态了。可以对特定的WebControl设置EnableViewState属性,也可以在页面一级设置:
<%@ Page EnableViewState="false" %>
2. 在恰当的时间点初始化控件属性
ASP.NET的控件在执行构造函数、初始化的期间设置的属性不会被跟踪变化;而在初始化阶段之后对属性的修改都会被跟踪,并最终记录到IE页面的__VIEWSTATE之中。所以,选择合理的初始化控件属性的执行点,能有效的减小页面尺寸。
3. 谨慎选择放到ViewState中的内容
放到ViewState中的内容会被序列化/反序列化,Asp.net为String、Integer、Boolean等基本类型的序列化做了优化,如果Array、ArrayList、 HashTable存储的是基本类型效率也较高,但其它类型则需要提供类型转换器(Type Converter),否则将使用代价昂贵的二进制序列化程序。