二十八条改善 ASP 性能和外观的技巧 7－18

#### 技巧 7: 将代码封装在 COM 对象中

如果您有许多 VBScript 或 JScript，您可以经常将代码移到编译的 COM 对象中，从而可改善性能。编译的代码通常比解释的代码运行得更快。编译的 COM 对象可以通过“早绑定”访问其它 COM 对象，与脚本使用的“晚绑定”相比，“早绑定”是调用 COM 对象的更有效方法。
将代码封装在 COM 对象中还有一些优点（除性能之外）：

- COM 对象有利于将表示逻辑与业务逻辑分开。
- COM 对象可以保证代码重复使用。
- 许多开发人员发现以 VB、C++ 或 Visual J++ 编写的代码比 ASP 更容易调试。

COM 对象也有缺点，包括初始开发时间和需要不同的程序设计技巧。注意封装少量的 ASP 可能引起性能下降，而不会得到性能改进。这种情况通常在少量的 ASP 代码被封装进 COM 对象时发生。在这种情况下，创建和调用 COM 对象的系统开销超过了编译的代码的优点。应反复地试验，以确定什么样的 ASP 脚本和 COM 对象代码的组合产生最好的性能。注意，与 Microsoft Windows NT&reg; 4.0/IIS 4.0 相比，Windows 2000/IIS 5.0 中在脚本和 ADO 性能方面有了很大的改进。因此，随着 IIS 5.0 的推出，编译代码比 ASP 代码的性能优势有所降低。
有关在 ASP 中使用 COM 的优点和缺点的详细讨论，参见 ASP Component Guidelines and Programming Distributed Applications with and Microsoft Visual Basic 6.0。如果您部署 COM 组件，以负荷对它们进行测试特别重要。事实上，理所当然应对所有的 ASP 应用程序进行负荷测试。

#### 技巧 8：迟一点获得资源，早一点释放资源

这里是一个小技巧供您参考。一般来说，最好迟一点获得资源，早一点释放资源。这适用于 COM 对象以及文件句柄和其它资源。
这种优化方法主要用于 ADO 连接和记录集。当您使用完记录集，比方说在显示一个表及其数据之后，应立即释放它，而不是等到页面结束时再释放。将 VBScript 变量设置为 Nothing 是最好的做法。不要让记录集超出作用域之外。而且，要释放任何相关的 Command 或 Connection 对象（在将记录集或连接设置为 = Nothing 之前，不要忘记调用 Close()）。这会缩短数据库必须为您准备资源的时间，并尽快释放数据库到连接池的连接。

#### 技巧 9：进程外执行过程以性能换取可靠性

ASP 和 MTS/COM+ 两者都有配置选项，可使您兼顾可靠性和性能。当建立和部署应用程序时，应知道如何兼顾两者的性能。

## ASP 选项

可以配置 ASP 应用程序，以便以三种方法之一运行。在 IIS 5.0 中，引入了“隔离级”这一术语以说明这些选项。这三个隔离级分别是低级、中级和高级：

- 低级隔离。这在 IIS 的所有版本中都得到支持，且是最快的。它在 Inetinfo.exe 中运行 ASP，Inetinfo.exe 是主要 IIS 进程。如果 ASP 应用程序崩溃，IIS 也会崩溃。（要在 IIS 4.0 下重新启动 IIS，Web 站点管理员应使用诸如 InetMon 之类的工具监视站点，如果服务器发生故障，应启用批处理文件以重新启动服务器。IIS 5.0 引入了可靠的重新启动，该方法可使发生故障的服务器自动重新启动。）
- 中级隔离。IIS 5.0 引入了这个新的级别，它被称为进程外级别，因为 ASP 在 IIS 进程之外运行。在中级隔离中，被配置作为中级隔离运行的所有 ASP 应用程序都共享一个进程空间。这就减少了在一台服务器运行多个进程外 ASP 应用程序所需要的进程数量。中级隔离是 IIS 5.0 中的默认隔离级别。
- 高级隔离。在 IIS 4.0 和 IIS 5.0 中支持这一级别，高级隔离也是进程外的。如果 ASP 崩溃，Web 服务器并不会崩溃。下次 ASP 请求时，ASP 应用程序就会自动重新启动。在高级隔离中，配置作为高级隔离运行的每个 ASP 应用程序都在其自有进程空间中运行。这样做可保护 ASP 应用程序彼此之间不相互干扰。其缺点是它要求每个 ASP 应用程序都要有一个单独的进程。当在一台服务器上必须运行许多应用程序时，系统开销就会大大增加。

哪个选项最好的呢？在 IIS 4.0 中，进程外运行将显著降低性能。在 IIS 5.0 中，做了许多改进，将进程外运行 ASP 应用程序所产生的开销降到最低限度。事实上，在绝大多数测试中，IIS 5.0 中的 ASP 进程外应用程序比 IIS 4.0 中的进程内应用程序运行得更快。不管怎样，在两个平台上，进程内（低隔离级）性能最佳。但是，如果访问率相对较低或最大吞吐量较低，低隔离级的优势不太明显。因此，在您每一 Web 服务器每秒钟需要数百或成千上万页面时，才会觉得有必要设置低隔离级。与往常一样，应对多种配置进行测试，确定您要采取哪一种折衷方案。
注意 当您运行 ASP 进程外应用程序时（中级或高级隔离），它们在 NT4 中的 MTS 和在 Windows 2000 中的 COM+ 中运行。即，在 NT4 中它们在 Mtx.exe 中运行；而在 Windows 2000 中，它们在 DllHost.exe 中运行。您可以在任务管理器中看到这些进程在运行。您还可以看到 IIS 如何为进程外 ASP 应用程序配置 MTS 程序包或 COM+ 应用程序。

### COM 选项

COM 组件也有三种配置选项，虽然与 ASP 选项不完全类似。COM 组件可以是“未配置的”、配置为库应用程序或配置为服务器应用程序。“未配置的”意思是指组件没有注册 COM+。组件将在调用程序的进程空间运行，那就是说，它们是“进程内的”。库应用程序也是进程内的，但使用 COM+ 的服务，包括安全、事务和上下文支持。服务器应用程序被配置为在它们自有的进程空间内运行。
您可以看到未配置的组件比库应用程序略有一些优势。库应用程序比服务器应用程序的性能优点更大。这是因为库应用程序与 ASP 在同一进程内运行，而服务器应用程序在它们的自有进程内运行。进程间的调用比进程内调用开销更大。而且，当在进程之间传递诸如记录集之类的数据时，必须在两个进程之间复制所有的数据。
陷阱！当使用 COM 服务器应用程序时，如果您在 ASP 和 COM 之间传递对象，要确保对象执行“按值汇集”或 MBV。执行 MBV 的对象将它们自己从一个进程复制到另一个进程。这比下面一种方法好，采用这种方法时，对象仍在创建者的进程中，另外一个进程反复地调用创建进程以使用该对象。切断连接的 ADO 记录集将“按值汇集”，连接的记录集则不然。Scripting.Dictionary 不执行 MBV，且不在进程之间传递。最后，VB 程序员请注意：MBV 不通过传递参数 ByVal 获得。MBV 由原始的组件作者执行。

#### 怎么办？

如果让我们建议一个兼顾性能与可靠性的合理配置，它们应是如下的配置：

- 在 IIS 4.0 中，使用 ASP 低隔离级别，使用 MTS 服务器程序包。
- 在 IIS 5.0 上，使用 ASP 的中隔离级，并使用 COM+ 库应用程序。

这些是非常一般的原则，主机服务公司一般情况下以中或高隔离级运行 ASP，而单用途的 Web 服务器可以以低隔离级运行。衡量各种利弊，并自己决定哪个配置更能符合您的需要。

#### 技巧 10：使用显式选项

在 .asp 文件中应使用 Option Explicit。此指令放在 .asp 文件的最上面，它强制开发人员声明要使用到的所有变量。许多程序员认为这种方法对于调试应用程序很有帮助，因为这种方法避免了键错变量名和误建新变量的可能性（例如，将 MyXMLString=) 错写成 MyXLMString=...。
更重要的一点也许是，声明的变量比未声明的变量速度更快。由此，脚本在运行时每次用到未声明的变量时，按名称引用它。另一方面，声明的变量是有顺序的，要么以编译时间，要么以运行时间。以后，声明的变量都按此顺序引用。因为 Option Explicit 强制变量声明，它能确保声明所有变量，因此访问的速度也很快。

#### 技巧 11：在子例程和函数中使用局部变量

局部变量是那些在子例程和函数内声明的变量。在函数或子例程内，局部变量访问比全局变量访问更快。局部变量的使用也会使代码更清晰，因此应尽量使用局部变量。

#### 技巧 12：将经常使用的数据复制到脚本变量中

当访问 ASP 中的 COM 对象时，应将经常使用的对象数据复制到脚本变量中。这样做可减少 COM 方法调用，因为 COM 方法调用与访问脚本变量相比，开销相对较大。当访问 Collection 和 Dictionary 对象时，这种技术也会减少开销很大的查找。
一般来说，如果您打算不止一次访问对象数据，那么就应将数据放到脚本变量中。这种优化的主要目标是 Request 变量（Form 和 QueryString 变量）。例如，您的站点可传递一个名为 UserID 的 QueryString 变量。假设此 UserID 在特定页面上被引用 12 次。可以无须调用 Request(?UserID?) 12 次，而是在 ASP 页面最上面将 UserID 指派到一个变量。然后在该页面自始至终使用该变量。这样就省去了 11 次 COM 方法调用。
实际上，访问 COM 属性或方法的开销并没有那么大。下面举一个例子，说明某相当常见的代码（从语法上讲）：
Foo.bar.blah.baz = Foo.bar.blah.qaz(1)
If Foo.bar.blah.zaq = Foo.bar.blah.abc Then ' ...
当此代码运行时，下面是发生的情况：

1. 变量 Foo 被解析为全局对象。
2. 变量 bar 被解析为 Foo 的成员。这实际就是一次 COM 方法调用。
3. 变量 blah 被解析为 Foo.bar 的成员。这又是一次 COM 方法调用。
4. 变量 qaz 被解析为 foo.bar.blah 的成员。没有错，这还是一次 COM 方法调用。
5. 调用 Foo.bar.blah.quaz(1)。再一次 COM 方法调用。懂了吗？
6. 再次执行步骤 1 至步骤 3 以解析 baz。系统并不知道调用 qaz 是否改变对象模型，因此必须再次执行步骤 1 至 3 以解析 baz。
7. 将 baz 解析为 Foo.bar.blah 的成员。赋予属性。
8. 再次执行步骤 1 至步骤 3 以解析 zaq。
9. 再次执行步骤 1 至步骤 3 以解析 abc。

正如您可看到的，效率相当差（且慢）。以 VBScript 写此代码的快速方法是：
Set myobj = Foo.bar.blah ' do the resolution of blah ONCE
Myobj.baz = myobj.qaz(1)
If Myobj.zaq = Myobj.abc Then '...
如果您使用 VBScript 5.0 或更高版本，您可以使用 With 语句写此代码：
With Foo.bar.blah
.baz = .qaz(1)
If .zaq = .abc Then '...
...
End With
注意此技巧也适用于 VB 程序设计。

#### 技巧 13：避免重新确定数组的维数

应尽量避免 Redim 数组。就性能而言，如果计算机的物理内存大小有限，最好将数组的初始维数设置为其最不利的情况 - 或将维数设置为其最佳的情况，然后再按需要重新确定维数。这并非意味着，如果知道您不需要内存时，就随便分配几兆字节的内存。
下面的代码给您显示使用 Dim 和 Redim 不当的情形。
<%
Dim MyArray()
Redim MyArray(2)
MyArray(0) = ?hello?
MyArray(1) = ?good-bye?
MyArray(2) = ?farewell?
...
' some other code where you end up needing more space happens, then ...
Redim Preserve MyArray(5)
MyArray(3) = ?more stuff?
MyArray(4) = ?even more stuff?
MyArray(5) = ?yet more stuff?
%>
最好一开始就将数组的初始大小 Dim 正确（在本例中，是 5）比 Redim 数组使其更大好得多。您可能浪费一些内存（如果您没有使用所有的元素），但获得的好处是速度变得更快。

#### 技巧 14：使用响应缓冲

您可以通过启用“响应缓冲”，将要输出的一整页缓冲起来。这样就将写到浏览器的量减到最少，从而改善总体性能。每个写操作都会产生很大的系统开销（在 IIS 中以及在通过网络发送的数据量方面），因此写操作越少越好。由于其启动慢且使用 Nagling 算法（用来减轻网络塞车情况），TCP/IP 在发送一些大的数据块时比必须发送许多小的数据块时的效率高得多。
有两个方法启用响应缓冲。第一种，您可以使用 Internet Services Manager 为整个应用程序启用响应缓冲。我们建议采用这种方法，在 IIS 4.0 和 IIS 5.0 中默认为新的 ASP 应用程序启用响应缓冲。第二种，可以在每个 ASP 页面的接近顶端的地方加入下面的代码行，从而启用响应缓冲：
<% Response.Buffer = True %>
此代码行必须在任何响应数据被写到浏览器之前执行（即，在任何 HTML 出现在 ASP 脚本之前以及在使用 Response.Cookies 集合设置任何 Cookies 之前）。一般来说，最好为整个应用程序启用响应缓冲。这样，您就不必在每个页面最上面写入上述的代码行。

## Response.Flush

关于响应缓冲有一个常见的抱怨，就是用户感觉到 ASP 页面的响应速度很慢（即使整个响应时间得到改进），因为他们必须等到整个页面生成，然后他们才能看到东西。对于运行时间长的页面，您可以设置 Response.Buffer = False，禁用响应缓冲。但是，一个更好的策略是利用 Response.Flush 方法。这种方法将 ASP 转换的所有 HTML 送到浏览器。例如，在转换 1,000 行的表的前 100 行之后，ASP 可以调用 Response.Flush，强制将转换的结果送到浏览器，这样可使用户在其余的行准备好之前看到头 100 行。这种技术可以将响应缓冲与浏览器逐渐显示数据完美地结合在一起。
（注意在上面的 1,000 行表的举例中，许多浏览器在它们看到关闭 </table> 标记之前不会开始显示表。检查您的目标浏览器是否支持。为避免这种情况，将表分成多个具有较少行的表，并在每个表之后调用 Response.Flush。较新版本的 Internet Explorer 在表完全下载之前就开始显示表，如果您指定表列宽，显示速度就会特别快，这样做可避免强制 Internet Explorer 通过测量每个单元格的内容宽度来计算列宽。）
另一个关于响应缓冲的常见的抱怨是，当产生非常大的页面时，将占用许多服务器内存。撇开产生大页面的方法不谈，这种问题也可通过巧妙使用 Response.Flush 来加以解决。

#### 技巧 15：批处理内嵌脚本和 Response.Write 语句

VBScript 语法 <% = expression %> 将“expression”的值写到 ASP 输出流中。如果响应缓冲未启用，那么执行其中的每一条语句，都会以许多小的数据包通过网络将数据写到浏览器中。这样速度很慢。而且穿插执行少量的脚本和 HTML，将引起脚本引擎和 HTML 之间的切换，从而降低性能。因此，使用下面的技巧：使用 Response.Write 调用代替捆绑紧密的内嵌表达式。例如，在下面的示例中，在每一行的每一字段对响应流有一次写操作，每一行在 VBScript 和 HTML 之间有许多切换：
<table>
<% For Each fld in rs.Fields %>
<th><% = fld.Name %></th>
<%
Next
While Not rs.EOF
%>
<tr>
<% For Each fld in rs.Fields %>
<td><% = fld.Value %></td>
<% Next
</tr>
<% rs.MoveNext
Wend %>
</table>
下面的代码更有效，每一行对响应流有一次写操作。所有的代码都包含在一个 VBScript 块内：
<table>
<%
For each fld in rs.Fields
Response.Write (?<th>? & fld.Name & ?</th>? & vbCrLf)
Next
While Not rs.EOF
Response.Write (?<tr>?)
For Each fld in rs.Fields %>
Response.Write(?<td>? & fld.Value & ?</td>? & vbCrLf)
Next
Response.Write ?</tr>?
Wend
%>
</table>
当禁用响应缓冲时，这一技巧的效果特别大。最好启用响应缓冲，然后看批处理 Response.Write 是否有助于提高性能。
（在这一特定举例中，建立表主体的嵌套循环 (While Not rs.EOF...) 可以用仔细构建的 GetString 调用来替代。）

#### 技巧 16：如果页面需要很长时间才能完成，那么执行前使用 Response.IsClientConnected

如果用户性急，他们可能会在您开始执行他们的请求之前，就会放弃 ASP 页面。如果他们单击刷新或移到服务器上的另一个页面，在 ASP 请求队列的末尾就有一个新的请求等候，在队列的中间有一个断开连接的请求。当服务器的负载很高时（因此请求队列就会很长，响应时间也会相应地变长），就会经常发生这种情况，这样只能使情况变得更糟。如果用户不再连接，执行 ASP 页面（特别是慢的、大的 ASP 页面）已没有任何意义。您可以使用 Response.IsClientConnected 属性检查这一情况。如果它返回 False，则应调用 Response.End 并放弃页的其余部分。事实上，IIS 5.0 已将这一做法编为程序 - 每当 ASP 即将执行新请求时，它就会检查请求在队列中已等候了多长时间。如果已经在那里等候了多于 3 秒钟，ASP 将检查客户机是否仍处于连接状态，如果没有连接，就立即终止请求。您可以在配置数据库中使用 AspQueueConnectionTestTime 设置将超时时间由 3 秒调整为其它值。
如果页面要花很长时间才能执行完，也可以不时地检查 Response.IsClientConnected。当启用了响应缓冲时，最好不时地执行 Response.Flush，以用户知道，正在发生什么事。
注意 在 IIS 4.0 上，除非先执行了 Response.Write，否则 Response.IsClientConnected 就不能正常工作。如果启用了缓冲，您也必须执行 Response.Flush。在 IIS 5.0 上，却没有必要这样做，- Response.IsClientConnected 工作正常。在任何情况下，Response.IsClientConnected 都会有一些开销，因此只有在一个操作至少要花（比方说） 500 毫秒（如果您想维持每秒钟数十页的吞吐量，这是一个很长的时间）才使用它。经验表明，不要每次重复执行紧密循环时都调用它，如显示表的许多行时 - 每隔二十或五十行调用一次可能比较合适。

#### 技巧 17：使用 <OBJECT> 标记例示对象

如果要引用不在所有代码路径（特别是服务器或应用程序作用域的对象）中使用的对象，使用 Global.asa 中 <object runat=server id=objname> 标记声明它们，而不使用 Server.CreateObject 方法。Server.CreateObject 能立即创建对象。如果以后不再使用该对象，您就浪费了资源。<object id=objname> 标记声明 objname，但在其方法或属性第一次使用以前，不会创建 objname。
这又是一个惰性计算的例子。

#### 技巧 18：对于 ADO 和其它组件使用 TypeLib 声明

当使用 ADO 时，开发人员经常加入 adovbs.txt，以访问 ADO 的各种常量。在要使用常量的每个页面中必须包含此文件。此常量文件相当大，给每个 ASP 页面的编译时间和脚本大小增加了许多系统开销。
IIS 5.0 引入了绑定到组件类型库的功能。这可使您引用类型库一次，并将其用在每个 ASP 页面上。每个页面不会产生编译常量文件的开销，且组件开发人员不必建立 VBScript#_include 文件以在 ASP 上使用。
要访问 ADO TypeLib，将下面一条语句放在 Global.asa 中。
<!-- METADATA NAME=?Microsoft ActiveX Data Objects 2.5 Library?
TYPE=?TypeLib? UUID=?{00000205-0000-0010-8000-00AA006D2EA4}? -->
或
<!-- METADATA TYPE=?TypeLib?
FILE=?C:\Program Files\Common Files\system\ado\msado15.dll? -->