ntwo

导航

ASP.NET站点性能提升-CPU

识别瓶颈

有多项技术可以在识别高CPU占用率代码:

  • 重点关注频繁执行的代码。循环,特别是嵌套循环,需要特别注意。如果对使用继承自IComparable类进行排序,这段代码会执行得非常频繁。如果从数据库获取数据,每一行数据都要处理,这也会使用到循环。
  • 可以使用轻量级的计数器,测量执行的频率和执行的时间。
  • 进行老式的调试。对网站进行压力测试,这样会使用很多CPU。减少一半代码,如果CPU使用率减少很多,那么问题就可能出在减少的那一半代码。继续,直到找到有问题的代码。
  • 尝试改变算法。

测算代码执行的时间,可以使用stopWatch类:

using System.Diagnostics;
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
// first bit of code ...
stopWatch.Stop();
TimeSpan elapsedTime1 = stopWatch.Elapsed;
stopWatch.Reset();
stopWatch.Start();
// second bit of code ...
stopWatch.Stop();
TimeSpan elapsedTime2 = stopWatch.Elapsed;

工具

有时,瓶颈隐藏的很深,这就需要使用分析工具。这些工具会降低网站的运行速度,所以在开发环境,而不要在生产环境使用它们。

数据访问

连接池

打开数据库连接是一个昂贵的的操作。所以ASP.NET维护了一个连接池。每一个连接字符串都有一个连接池。

当打开一个连接时,从连接池得到一个连接。当关闭那个连接时,它依然是打开的,归还连接池,准备给另一个线程使用。

为每一个需要访问的数据库,只使用一个连接字符串。

在web.config中存储连接字符串:

<configuration>
    <connectionStrings>
      <add name="ConnectionString" connectionString="....."/>
    </connectionStrings>
</configuration>

在代码中获取连接字符串:

using System.Configuration;
...
string connectionString =
    ConfigurationManager.ConnectionStrings[
        "ConnectionString"].ConnectionString;

连接池由连接字符串参数控制:

Max Pool Size:连接池的最大值。默认值是100。如果连接池中的连接达到最大值,连接请求将会排队。

Min Pool Size:当连接池创建时连接的初始数量。默认值是0。这样,最初的数据库访问会因为创建新连接被延迟。为了減少这些延迟,可以设置参数为预计的程序最终需要的连接数量。

Connect Timeout:在终止并产生错误前请求等待连接的时间,单位是秒。默认值是15秒。

Pooling:是否使用连接池。默认值是true。设置为false关闭连接池。

为什么要关闭连接池?是为了在应用程序发生连接泄露时,发生崩溃。

考虑以下代码:

SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
... code that may throw an exception ...
connection.Close();

如果代码抛出异常,连接就不会关闭。最终,它会被垃圾收集器关闭,但这会需要一段时间,这要看内存压力。同时,越来越多的连接会加到连接池中。

当达到连接池的最大值时,请求会等待连接,直到超时,最终被终止。

最好的方法是修改代码,这样即使在有异常的情况下,连接也会关闭:

 

using (SqlConnection connection = new  
  SqlConnection(connectionString))
{
    connection.Open();
    ... code that may throw an exception ...
}
使用计数器观察当前打开的连接数量,如果有流量稳定的情况下,连接数量保持增长,说明可能有连接泄露。
分类:SQL Server General Statistics
User Connections:当前连接到系统的用户数。

DataSet和List

当从数据库中读取数据时,可能需要将数据存储在集合中。这样,就可以缓存或者将它传递到另一层。

最简单的集合是使用DataSet,只需使用少量代码就可以填充DataSet,并且DataSet有很多内置的功能。另一种选择是泛型List,这需要自己填充,也没有很多内置功能,但它是轻量级的。

如果不需要DataSet提供的功能,使用泛型List可以节省可见的CPU周期。

返回多结果集

ADO.NET允许一次获取多个结果集。例如:

CREATE PROCEDURE [dbo].[GetBooksAndAuthors]
AS
BEGIN
  SET NOCOUNT ON;
  SELECT [BookId]
    ,[Title]
    ,[AuthorId]
    ,[Price]
  FROM [dbo].[Book]
  SELECT [AuthorId]
    ,[Name]
    ,[Address]
    ,[Phone]
    ,[Email]
  FROM [dbo].[Author]
END
 
使用SqlDataReader.NextResult访问第二个结果集:
 
using (SqlDataReader reader = cmd.ExecuteReader())
{
    while (reader.Read())
    {
        // read first result set ...
    }
    reader.NextResult();
    while (reader.Read())
    {
        // read second result set ...
    }
}
 
如果有一个页面上需要显示多个结果集,可一次读取它们。

一次连接中发送多个Insert语句

在批量插入数据时,可以一次执行多条Insert语句。

创建SQL参数:

const string singleExec = "EXEC dbo.InsertData @Title{0}, @Author{0}, @Price{0};";
StringBuilder sql = new StringBuilder();
for (int j = 0; j < nbrInserts; j++)
{
    sql.AppendFormat(singleExec, j);
}
为参数赋值:
for (int j = 0; j < nbrInserts; j++)
{
    cmd.Parameters.AddWithValue("@Title" + j.ToString(), ...);
    cmd.Parameters.AddWithValue("@Author" + j.ToString(), ...);
    cmd.Parameters.AddWithValue("@Price" + j.ToString(), ...);
}
Then send all EXEC statements in one go to the database:
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();

注意command text包括SQL文本(EXEC语句),而不是存储过程名。所以使用CommandType.Text。

使用native data providers

使用native data providers,例如System.Data.SqlClient或者System.Data.OracleClient,而不要使用System.Data.OleDb和System.Data.ODBC,因为native data providers较少抽象,所以效率更高,

异常

抛出异常是非常昂贵的。当你抛出一个异常,首先需要在堆上创建异常对象。异常对象必需包括调用堆栈,这由运行时创建。然后,运行时查找合适的异常处理器,执行它。最后,还要执行finally代码块。只在真正出现异常的情况下使用异常,不要在正常程序流程中。

计数器

分类:NET CLR Exceptions

# of Exceps Thrown:从程序启动开始抛出的异常数量。

# of Exceps Thrown/sec:每秒抛出的异常数量。

# of Filters/sec:每秒执行的.NET异常过滤器数量。异常过滤器判断是否应该处理异常。

# of Finallys/sec:每秒执行的finally程序块。

Throw To Catch Depth/sec:每秒从抛出异常的帧到处理异常的帧间的堆栈帧。

DataBinder.Eval

例如GridView和Repeater的控件使用模板。常用的方法是在模板中使用DataBinder.Eval引用字段:

<asp:Repeater ID="rptrEval" runat="server">
    <ItemTemplate>
        <%# Eval("field1")%>
        <%# Eval("field2")%>
        <%# Eval("field3")%>
        <%# Eval("field4")%>
    </ItemTemplate>
</asp:Repeater>

这种方法使用反射。如果有10行,每行4个字段,就执行了40次Eval。

更快的方法是直接引用包含字段的类:

<asp:Repeater ID="rptrCast" runat="server">
    <ItemTemplate>
        <%# ((MyClass)Container.DataItem).field1 %>
        <%# ((MyClass)Container.DataItem).field2 %>
        <%# ((MyClass)Container.DataItem).field3 %>
        <%# ((MyClass)Container.DataItem).field4 %>
    </ItemTemplate>
</asp:Repeater>

如果使用DataTable,强制转换成DataRowView。

垃圾收集

使用.NET CLR Memmory分类中的% Time in GC计算器可以查看垃圾收集占用的时间。

线程

如果滥用线程,可能会浪费很多CPU周期。如果你的程序是多线程的,考虑以下方法减少线程耗费:

  • 如果使用线程等待数据库或web service之类的资源,可能使用异步请求。
  • 不要自己创建线程。使用ThreadPool.QueueUserWorkItem从线程池中获得线程。
  • 不要使用线程进行CPU密集型的任务。如果你有4个CPU,同时进行4个以上的CPU密集型任务是没有意义的。
  • 減少同时运行的线程数量。线程切换很昂贵。

使用以下计数器获取正在运行的线程信息:

分类:Thread

% Processor Time:显示每个线程使用的处理器时间百分比。

Context Switches/sec:显示每个线程每秒的上下文切换比率。

StringBuilder

参考内存优化中StringBuilder使用。

正则表达式初始化

当使用正则表达式匹配字符串时,有两种方案。方案一,可以首先使用正则表达式初始化Regex对象,然后使用这个对象的IsMatch方法。方案二,直接调用静态方法Regex.IsMatch。

如果需要进行多次相同的匹配,最好在循环外初始化Regex对象,而不要使用IsMatch的静态版本。

如果网站需要很多次匹配正则表达式,你可以创建一个包含这正则表达式对象的静态类。这样,就只在程序启动时初始化,而不是在每次请求时都要初始化。

UtcNow

如果只是比较日期,而不用显示给访问者,DateTime.UtcNow要比DateTime.Now快得多。

Foreach

C#语句foreach是一种遍历集合的便利方法,但因为使用enumerator,它要比通常的for循环要慢。

虚属性

如果定义类中的属性为virtual,就是允许派生类重写这个属性。但是,这样会减慢属性访问,因为编译器会不再内联它。

所以,如果你读虚属性多次,你可以将属性拷贝到本地变量中,这样可以提高性能。

避免不必要的处理

  • 早验证。确保在处理数据之前,数据是正确的。
  • 在进行昂贵操作前,检查用户是否还在线。使用Response.IsClientConnected检查。
  • 检查Page.IsPostBack,这样就不会重新生成已经在ViewState中的数据了。

缩短HTTP管道

如果不需要一些HTTP modules,可以在web.config中移除它们。例如:

<system.web>
    <httpModules>
        <remove name="RoleManager" />
    </httpModules>
</system.web>

更多资源

posted on 2010-11-18 17:30  9527  阅读(972)  评论(0编辑  收藏  举报